diff --git a/.github/workflows/lint-build-test.yml b/.github/workflows/lint-build-test.yml
index cf955b1..dcdd559 100644
--- a/.github/workflows/lint-build-test.yml
+++ b/.github/workflows/lint-build-test.yml
@@ -62,3 +62,9 @@ jobs:
- name: Test
run: pnpm test
+
+ - name: Install Playwright browsers
+ run: pnpm exec playwright install --with-deps
+
+ - name: UI Integration Tests
+ run: pnpm test:e2e
diff --git a/README.md b/README.md
index 0d71531..30dc19f 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,6 @@ A monorepo is a single repository that contains multiple projects. This allows u
This repository is an amalgamation of all the tools and applications that make DiceDB fun and easy to use in the real world. The name is a nod to this amalgamation, and inspired by the [alloy](https://en.wikipedia.org/wiki/Alloy).
-
## What's inside?
This monorepo includes the following packages/apps:
@@ -29,6 +28,7 @@ This monorepo includes the following packages/apps:
### Prerequisites
Ensure you have the following installed:
+
- node.js (v18.17.0 or later)
- pnpm (v9.10.0 or later)
@@ -43,8 +43,6 @@ npm install -g pnpm@9.10.0
> If you're unfamiliar with pnpm, it’s an alternative package manager that is faster and more efficient than npm. Learn more about pnpm [here](https://pnpm.io/).
-
-
### Installation
Clone the repository and install the dependencies:
@@ -102,7 +100,6 @@ These commands will not only start the development server for the package reques
> We also have a `pnpm dev:playground` alias that does the same thing as `pnpm dev --filter @dicedb/playground-web` for convenience.
-
### Testing
To run tests for all apps and packages, run the following command:
@@ -121,7 +118,13 @@ cd alloy
pnpm test:watch
```
+### Integration Test
+To run E2E test, run the following command:
+
+```
+pnpm test:e2e
+```
### Formatting
@@ -144,6 +147,7 @@ pnpm lint
## The Monorepo Structure
The monorepo is divided into 3 main directories:
+
- `apps`: contains all the applications i.e. deployable units
- `packages`: contains all the packages i.e. reusable code across the apps
- `tooling`: contains all the configurations and tooling used across the monorepo
diff --git a/apps/playground-web/.gitignore b/apps/playground-web/.gitignore
index f886745..3057931 100644
--- a/apps/playground-web/.gitignore
+++ b/apps/playground-web/.gitignore
@@ -34,3 +34,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
\ No newline at end of file
diff --git a/apps/playground-web/components/Shell/Shell.tsx b/apps/playground-web/components/Shell/Shell.tsx
index 96768ee..4dfc6aa 100644
--- a/apps/playground-web/components/Shell/Shell.tsx
+++ b/apps/playground-web/components/Shell/Shell.tsx
@@ -27,7 +27,7 @@ export default function Shell({ decreaseCommandsLeft }: ShellProps) {
{output.map((line, index) => (
{line}
diff --git a/apps/playground-web/components/Shell/__tests__/index.test.tsx b/apps/playground-web/components/Shell/__tests__/index.test.tsx
index 1fd31f8..9d73ce4 100644
--- a/apps/playground-web/components/Shell/__tests__/index.test.tsx
+++ b/apps/playground-web/components/Shell/__tests__/index.test.tsx
@@ -60,7 +60,7 @@ describe('Shell Component', () => {
const { cliInputElement, user, getByTestId } = setupTest();
await user.type(cliInputElement, 'EXEC{enter}');
- const terminalOutputElement = getByTestId('terminal-output');
+ const terminalOutputElement = getByTestId('terminal-output-1');
expect(terminalOutputElement).toHaveTextContent(
"(error) ERR unknown command 'EXEC'",
);
diff --git a/apps/playground-web/docker-compose.dev.yml b/apps/playground-web/docker-compose.dev.yml
index 04a797b..155dd0c 100644
--- a/apps/playground-web/docker-compose.dev.yml
+++ b/apps/playground-web/docker-compose.dev.yml
@@ -15,7 +15,7 @@ services:
depends_on:
- dicedb
environment:
- - DICE_ADDR=dicedb:7379
+ - DICEDB_ADDR=dicedb:7379
frontend:
build:
diff --git a/apps/playground-web/jest.config.mjs b/apps/playground-web/jest.config.mjs
index 36c3619..d6d8657 100644
--- a/apps/playground-web/jest.config.mjs
+++ b/apps/playground-web/jest.config.mjs
@@ -1,8 +1,8 @@
-import nextJest from "next/jest.js";
+import nextJest from 'next/jest.js';
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
- dir: "./",
+ dir: './',
});
// Add any custom config to be passed to Jest
@@ -10,13 +10,13 @@ const createJestConfig = nextJest({
* @type {import('@jest').Config}
*/
const config = {
- coverageProvider: "v8",
- testEnvironment: "jsdom",
+ coverageProvider: 'v8',
+ testEnvironment: 'jsdom',
moduleNameMapper: {
- "^@/(.*)$": "/$1",
+ '^@/(.*)$': '/$1',
},
// Add more setup options before each test is run
- setupFilesAfterEnv: ["/jest.setup.mjs"],
+ setupFilesAfterEnv: ['/jest.setup.mjs'],
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
diff --git a/apps/playground-web/package.json b/apps/playground-web/package.json
index 1482284..bd5ff70 100644
--- a/apps/playground-web/package.json
+++ b/apps/playground-web/package.json
@@ -12,9 +12,11 @@
"lint": "eslint . --ext js,jsx,ts,tsx",
"type-check": "tsc --noEmit",
"prettier:format": "prettier -c ../../.prettierrc --write \"**/*.{js,jsx,ts,tsx,json,css}\"",
- "test": "jest",
+ "test": "jest --testPathIgnorePatterns=\"/tests/\"",
"test:watch": "jest --watch",
- "test:coverage": "jest --coverage"
+ "test:coverage": "jest --coverage",
+ "test:e2e": "npx playwright test",
+ "test:e2e-report": "npx playwright show-report"
},
"dependencies": {
"@emotion/react": "^11.13.3",
@@ -32,6 +34,7 @@
"@dicedb/ui": "workspace:*"
},
"devDependencies": {
+ "@playwright/test": "^1.47.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
diff --git a/apps/playground-web/playwright.config.ts b/apps/playground-web/playwright.config.ts
new file mode 100644
index 0000000..20d55e9
--- /dev/null
+++ b/apps/playground-web/playwright.config.ts
@@ -0,0 +1,47 @@
+import { defineConfig, devices } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://127.0.0.1:3000',
+ reuseExistingServer: !process.env.CI,
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ timeout: 30000,
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ timeout: 30000,
+ },
+
+ {
+ name: 'webkit',
+ use: { ...devices['Desktop Safari'] },
+ timeout: 30000,
+ },
+ ],
+});
diff --git a/apps/playground-web/tests/playground.test.ts b/apps/playground-web/tests/playground.test.ts
new file mode 100644
index 0000000..d600808
--- /dev/null
+++ b/apps/playground-web/tests/playground.test.ts
@@ -0,0 +1,104 @@
+import { test, expect } from '@playwright/test';
+import type { Page } from '@playwright/test';
+
+const runCommand = async (page: Page, cmd: string) => {
+ const cmdInput = page.getByTestId('shell-input');
+ await cmdInput.fill(cmd);
+ await page.keyboard.press('Enter');
+};
+
+test.describe('[Playground Component]', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('http://localhost:3000');
+ const cmdInput = page.getByTestId('shell-input');
+ await expect(cmdInput).toBeVisible();
+ });
+
+ test('should execute SET command properly', async ({ page }) => {
+ let outputIdx = 0;
+
+ // Happy case
+ await runCommand(page, 'SET foo bar');
+ // Adding 2 to outputIndex after each command execution
+ // Reason: 2 items are added to output after each execution, 1st is command itself and 2nd is its result
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ 'OK',
+ );
+
+ // Error case: SET with wrong number of arguments
+ await runCommand(page, 'SET foo');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ "(error) ERR wrong number of arguments for 'set' command",
+ );
+ });
+
+ test('should execute GET command properly', async ({ page }) => {
+ let outputIdx = 0;
+
+ // Happy case
+ await runCommand(page, 'SET foo bar');
+ outputIdx += 2;
+ await runCommand(page, 'GET foo');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ 'bar',
+ );
+
+ // Error case for wrong key get
+ await runCommand(page, 'GET foo1');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ '(nil)',
+ );
+
+ // Error case: GET with wrong number of arguments
+ await runCommand(page, 'GET foo bar');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ "(error) ERR wrong number of arguments for 'get' command",
+ );
+ });
+
+ test('should execute DEL command properly', async ({ page }) => {
+ let outputIdx = 0;
+
+ // Happy case
+ await runCommand(page, 'SET foo bar');
+ outputIdx += 2;
+ await runCommand(page, 'GET foo');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ 'bar',
+ );
+ await runCommand(page, 'DEL foo');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ '1',
+ );
+ await runCommand(page, 'GET foo');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ // Getting back the deleted key should return (nil) output
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ '(nil)',
+ );
+
+ // Error case: DEL key which is not present
+ await runCommand(page, 'DEL bar');
+ outputIdx += 2;
+ await page.getByTestId(`terminal-output-${outputIdx}`).waitFor();
+ await expect(page.getByTestId(`terminal-output-${outputIdx}`)).toHaveText(
+ '0',
+ );
+ });
+});
diff --git a/package.json b/package.json
index 62fd8a0..5e30183 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,8 @@
"lint": "turbo lint",
"format": "prettier -c ./.prettierrc --write \"**/*.{js,jsx,ts,tsx,json,css}\"",
"check:format": "prettier -c ./.prettierrc \"**/*.{js,jsx,ts,tsx,json,css}\"",
- "test": "turbo test"
+ "test": "turbo test",
+ "test:e2e": "turbo test:e2e"
},
"devDependencies": {
"prettier": "^3.2.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4c2c16f..8e7e7b8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,7 +43,7 @@ importers:
version: 0.446.0(react@18.3.1)
next:
specifier: 14.2.13
- version: 14.2.13(@babel/core@7.25.7)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: 14.2.13(@babel/core@7.25.7)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
package.json:
specifier: ^2.0.1
version: 2.0.1
@@ -69,6 +69,9 @@ importers:
'@dicedb/typescript-config':
specifier: workspace:*
version: link:../../tooling/typescript-config
+ '@playwright/test':
+ specifier: ^1.47.2
+ version: 1.47.2
'@testing-library/dom':
specifier: ^10.4.0
version: 10.4.0
@@ -912,6 +915,11 @@ packages:
resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+ '@playwright/test@1.47.2':
+ resolution: {integrity: sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
'@popperjs/core@2.11.8':
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
@@ -2235,6 +2243,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3381,6 +3394,16 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
+ playwright-core@1.47.2:
+ resolution: {integrity: sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ playwright@1.47.2:
+ resolution: {integrity: sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
@@ -5166,6 +5189,10 @@ snapshots:
picocolors: 1.1.0
tslib: 2.6.2
+ '@playwright/test@1.47.2':
+ dependencies:
+ playwright: 1.47.2
+
'@popperjs/core@2.11.8': {}
'@rushstack/eslint-patch@1.5.1': {}
@@ -6888,6 +6915,9 @@ snapshots:
fs.realpath@1.0.0: {}
+ fsevents@2.3.2:
+ optional: true
+
fsevents@2.3.3:
optional: true
@@ -8006,7 +8036,7 @@ snapshots:
netmask@2.0.2: {}
- next@14.2.13(@babel/core@7.25.7)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ next@14.2.13(@babel/core@7.25.7)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@next/env': 14.2.13
'@swc/helpers': 0.5.5
@@ -8027,6 +8057,7 @@ snapshots:
'@next/swc-win32-arm64-msvc': 14.2.13
'@next/swc-win32-ia32-msvc': 14.2.13
'@next/swc-win32-x64-msvc': 14.2.13
+ '@playwright/test': 1.47.2
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -8322,6 +8353,14 @@ snapshots:
dependencies:
find-up: 4.1.0
+ playwright-core@1.47.2: {}
+
+ playwright@1.47.2:
+ dependencies:
+ playwright-core: 1.47.2
+ optionalDependencies:
+ fsevents: 2.3.2
+
pluralize@8.0.0: {}
postcss-import@15.1.0(postcss@8.4.31):
diff --git a/turbo.json b/turbo.json
index e4f97c8..69514b4 100644
--- a/turbo.json
+++ b/turbo.json
@@ -24,6 +24,7 @@
"test:watch": {
"cache": false,
"persistent": true
- }
+ },
+ "test:e2e": {}
}
}