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": {} } }