From bb084867fac3aa73a233e3527db19b72719a3b35 Mon Sep 17 00:00:00 2001 From: Sidharth Vinod Date: Mon, 17 Mar 2025 19:47:13 -0700 Subject: [PATCH] chore: Add playwright tests --- .github/workflows/tests.yml | 41 +++++++------ .gitignore | 6 ++ package.json | 8 ++- playwright.config.ts | 26 +++++++++ pnpm-lock.yaml | 38 ++++++++++++ tests/actions.spec.ts | 47 +++++++++++++++ tests/diagramUpdate.spec.ts | 69 ++++++++++++++++++++++ tests/docMap.spec.ts | 42 ++++++++++++++ tests/history.spec.ts | 84 +++++++++++++++++++++++++++ tests/loadSite.spec.ts | 113 ++++++++++++++++++++++++++++++++++++ tests/themes.spec.ts | 46 +++++++++++++++ tests/utils.ts | 61 +++++++++++++++++++ tsconfig.json | 6 +- 13 files changed, 564 insertions(+), 23 deletions(-) create mode 100644 playwright.config.ts create mode 100644 tests/actions.spec.ts create mode 100644 tests/diagramUpdate.spec.ts create mode 100644 tests/docMap.spec.ts create mode 100644 tests/history.spec.ts create mode 100644 tests/loadSite.spec.ts create mode 100644 tests/themes.spec.ts create mode 100644 tests/utils.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index edcd3ebb..b333b6bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,13 +7,11 @@ on: - develop jobs: - cypress-run: + playwright: + name: 'Playwright Tests' runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - # run 3 copies of the current job in parallel - containers: [1, 2, 3] + container: + image: mcr.microsoft.com/playwright:v1.42.1-jammy steps: - name: Checkout @@ -23,7 +21,6 @@ jobs: id: pnpm-and-build-cache with: path: | - ~/.cache/Cypress build node_modules key: ${{ runner.os }}-node_modules-build-${{ hashFiles('**/pnpm-lock.yaml') }} @@ -38,16 +35,22 @@ jobs: node-version-file: '.node-version' cache: 'pnpm' - # Install NPM dependencies, cache them correctly - # and run all Cypress tests - - name: Cypress run - uses: cypress-io/github-action@v6 - with: - build: pnpm build - start: pnpm preview - wait-on: 'http://localhost:3000' - record: true - parallel: true + - name: Install dependencies + run: pnpm install + + - name: Build + run: pnpm build + + - name: Run Playwright tests + run: | + pnpm preview & + pnpm test:e2e env: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - CYPRESS_CI: true + CI: true + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index b3f2edff..53da7187 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ /functions /node_modules /snapshots.js + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package.json b/package.json index d355a442..33d31f96 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,10 @@ "test:coverage": "vitest run --coverage", "test:browser": "cypress run", "test": "pnpm test:unit && pnpm test:browser", - "cy": "cypress open" + "cy": "cypress open", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug" }, "devDependencies": { "@cypress/snapshot": "2.1.7", @@ -69,7 +72,8 @@ "typescript": "^5.5.4", "vite": "^5.4.14", "vitest": "^2.1.9", - "vitest-dom": "^0.1.1" + "vitest-dom": "^0.1.1", + "@playwright/test": "^1.42.1" }, "dependencies": { "@mermaid-js/layout-elk": "^0.1.7", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..751060e8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + viewport: { width: 1920, height: 768 } + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] } + } + ], + webServer: { + command: 'pnpm run dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI + } +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f097a69..44d64314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@fortawesome/fontawesome-free': specifier: ^6.5.2 version: 6.7.2 + '@playwright/test': + specifier: ^1.42.1 + version: 1.51.1 '@sveltejs/adapter-static': specifier: 3.0.8 version: 3.0.8(@sveltejs/kit@2.20.0(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.23.1)(vite@5.4.14))(svelte@5.23.1)(vite@5.4.14)) @@ -571,6 +574,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.51.1': + resolution: {integrity: sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==} + engines: {node: '>=18'} + hasBin: true + '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} @@ -2209,6 +2217,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} @@ -3059,6 +3072,16 @@ packages: resolution: {integrity: sha512-hMhneYm3GCPyQon88SZrVJx+LlqhM1kZFQbuAgXPoh/Az2YvO1B6bitT9qlhpiTdJlsT5lsr3gPmzoVjb5CDXA==} engines: {node: '>=10'} + playwright-core@1.51.1: + resolution: {integrity: sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.51.1: + resolution: {integrity: sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -4650,6 +4673,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.51.1': + dependencies: + playwright: 1.51.1 + '@polka/url@1.0.0-next.28': {} '@rollup/rollup-android-arm-eabi@4.35.0': @@ -6525,6 +6552,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7350,6 +7380,14 @@ snapshots: plausible-tracker@0.3.9: {} + playwright-core@1.51.1: {} + + playwright@1.51.1: + dependencies: + playwright-core: 1.51.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} points-on-curve@0.2.0: {} diff --git a/tests/actions.spec.ts b/tests/actions.spec.ts new file mode 100644 index 00000000..905e6ddd --- /dev/null +++ b/tests/actions.spec.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; +import { typeInEditor, verifyFileSizeGreaterThan } from './utils'; + +test.describe('Check actions', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/edit'); + await page.getByText('Actions', { exact: true }).click(); + }); + + test('should update markdown code', async ({ page }) => { + const markdown = page.locator('#markdown'); + const oldText = await markdown.inputValue(); + await typeInEditor(page, 'C --> HistoryTest', { bottom: true, newline: true }); + const newText = await markdown.inputValue(); + expect(newText).not.toBe(oldText); + }); + + test.skip('should load gists from URL', async ({ page }) => { + await page + .locator('#gist') + .fill('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a'); + await page.getByText('Load Gist').click(); + await expect(page.getByText('Go shopping!!')).toBeVisible(); + }); + + test('should download png and svg', async ({ page }) => { + const downloadPNGPromise = verifyFileSizeGreaterThan(page, 'diagram', 'png', 29_000); + await page.locator('#downloadPNG').click(); + await downloadPNGPromise; + + const downloadSVGPromise = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 10_000); + await page.locator('#downloadSVG').click(); + await downloadSVGPromise; + + // Verify downloaded file is different for different diagrams + await page.getByText('Sample Diagrams').click(); + await page.getByText('ER', { exact: true }).click(); + + const downloadPNGPromise2 = verifyFileSizeGreaterThan(page, 'diagram', 'png', 35_000); + await page.locator('#downloadPNG').click(); + await downloadPNGPromise2; + + const downloadSVGPromise2 = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 11_000); + await page.locator('#downloadSVG').click(); + await downloadSVGPromise2; + }); +}); diff --git a/tests/diagramUpdate.spec.ts b/tests/diagramUpdate.spec.ts new file mode 100644 index 00000000..c0b03a92 --- /dev/null +++ b/tests/diagramUpdate.spec.ts @@ -0,0 +1,69 @@ +import { expect, test } from '@playwright/test'; +import { typeInEditor } from './utils'; + +test.describe('Auto sync tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/.*\/edit#pako/); + }); + + test('should dim diagram when code is edited', async ({ page }) => { + await page.getByText('Auto sync').click(); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + await expect(page.locator('#errorContainer')).not.toBeVisible(); + await typeInEditor(page, ' C --> Test', { bottom: true }); + await expect(page.locator('#view')).toHaveClass(/outOfSync/); + await expect(page.locator('#errorContainer')).toContainText('Diagram out of sync.'); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); + + test.skip('should update diagram when shortcut is used', async ({ page }) => { + await page.getByText('Auto sync').click(); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + await typeInEditor(page, ' C --> Test'); + await expect(page.locator('#view')).toHaveClass(/outOfSync/); + await expect(page.locator('#errorContainer')).toContainText('Diagram out of sync.'); + await page.locator('#editor').click(); + await page.keyboard.press('ControlOrMeta+Enter'); + await page.keyboard.press('ControlOrMeta+Enter'); + await expect(page.locator('#errorContainer')).not.toBeVisible(); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + }); + + test('should show/hide sync button with auto sync', async ({ page }) => { + await expect(page.locator('[data-cy=sync]')).not.toBeVisible(); + await page.getByText('Auto sync').click(); + await expect(page.locator('[data-cy=sync]')).toBeVisible(); + await page.locator('#autoSync').check(); + await expect(page.locator('[data-cy=sync]')).not.toBeVisible(); + }); + + test('should not dim diagram when code is in sync', async ({ page }) => { + await page.getByText('Auto sync').click(); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + await typeInEditor(page, ' C --> Test'); + await expect(page.locator('#view')).toHaveClass(/outOfSync/); + await page.locator('[data-cy=sync]').click(); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + await page.locator('#autoSync').check(); + await typeInEditor(page, 'ing'); + await expect(page.locator('#view')).not.toHaveClass(/outOfSync/); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); +}); + +test.describe('Pan and Zoom', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should toggle pan and zoom', async ({ page }) => { + await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).not.toBeVisible(); + await page.getByText('Pan & Zoom').click(); + await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).toBeVisible(); + await page.getByText('Pan & Zoom').click(); + await expect(page.locator('#svg-pan-zoom-reset-pan-zoom')).not.toBeVisible(); + }); +}); diff --git a/tests/docMap.spec.ts b/tests/docMap.spec.ts new file mode 100644 index 00000000..d7c35daf --- /dev/null +++ b/tests/docMap.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Editor docs tests', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/edit'); + await page.getByText('Sample Diagrams').click(); + }); + + test('Test default loading', async ({ page }) => { + await expect(page.locator('[data-cy=docs][href^="https://mermaid.js.org/"]')).toBeVisible(); + }); + + test('Test to see if the correct URL loads when changing from one diagram to other', async ({ + page + }) => { + await page.getByText('Flow', { exact: true }).click(); + await expect(page.locator('[data-cy=docs][href$="/syntax/flowchart.html"]')).toBeVisible(); + + await page.getByText('Config').click(); + await expect( + page.locator('[data-cy=docs][href$="/syntax/flowchart.html#configuration"]') + ).toBeVisible(); + + await page.getByText('Sequence').click(); + await expect( + page.locator('[data-cy=docs][href$="/syntax/sequenceDiagram.html#configuration"]') + ).toBeVisible(); + + await page.getByText('Code', { exact: true }).click(); + await expect( + page.locator('[data-cy=docs][href$="/syntax/sequenceDiagram.html"]') + ).toBeVisible(); + }); + + test("Test to check URLs for a case where config URL doesn't exist", async ({ page }) => { + await page.getByText('State').click(); + await expect(page.locator('[data-cy=docs][href$="/syntax/stateDiagram.html"]')).toBeVisible(); + + await page.getByText('Config').click(); + await expect(page.locator('[data-cy=docs][href$="/syntax/stateDiagram.html"]')).toBeVisible(); + }); +}); diff --git a/tests/history.spec.ts b/tests/history.spec.ts new file mode 100644 index 00000000..6b0d003d --- /dev/null +++ b/tests/history.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from '@playwright/test'; +import { typeInEditor } from './utils'; + +test.describe('Save History', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => { + Object.defineProperty(Date, 'now', { + value: () => new Date(2022, 0, 1).getTime() + }); + }); + await page.goto('/edit'); + await page.getByText('History').click(); + }); + + test('should load history from localstorage', async ({ page }) => { + await page.evaluate(() => { + localStorage.setItem( + 'manualHistoryStore', + '[{"state":{"code":"graph TD\\n A[Halloween] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"manual","id":"d7ea820e-21dd-418a-b984-fd58acde09df","name":"hollow-art"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"b749ffc6-522b-4a44-86cf-7c1ffc3146b3","name":"helpful-ocean"}]' + ); + localStorage.setItem( + 'autoHistoryStore', + '[{"state":{"code":"graph TD\\n A[New Year] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":false},"time":0,"type":"auto","id":"69ea820e-522b-4a44-86cf-fd58acde09df","name":"barking-dog"},{"state":{"code":"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)","mermaid":"{\\n \\"theme\\": \\"dark\\"\\n}","autoSync":true,"updateDiagram":true},"time":0,"type":"manual","id":"x749ffc6-21dd-418a-b984-7c1ffc3146b3","name":"needy-mosquito"}]' + ); + }); + await page.reload(); + await page.getByText('History').click(); + await expect(page.locator('#historyList li')).toHaveCount(2); + await expect(page.locator('#historyList').getByText('No items in History')).not.toBeVisible(); + await expect(page.locator('#historyList')).toContainText('helpful-ocean'); + await expect(page.locator('#historyList')).toContainText('hollow-art'); + await page.getByText('Restore').first().click(); + await expect(page.locator('#view').getByText('Halloween')).toBeVisible(); + await page.getByText('Timeline').click(); + + await expect(page.locator('#historyList li')).toHaveCount(2); + await expect(page.locator('#historyList').getByText('No items in History')).not.toBeVisible(); + await expect(page.locator('#historyList')).toContainText('needy-mosquito'); + await expect(page.locator('#historyList')).toContainText('barking-dog'); + await page.getByText('Restore').first().click(); + await expect(page.locator('#view').getByText('New Year')).toBeVisible(); + }); + + test.skip('should save when clicked', async ({ page }) => { + await expect(page.locator('#historyList li')).toHaveCount(0); + await expect(page.locator('#historyList')).toContainText('No items in History'); + await page.locator('#saveHistory').click(); + await expect(page.locator('#historyList').getByText('No items in History')).not.toBeVisible(); + await expect(page.locator('#historyList li')).toHaveCount(1); + const dialogPromise = page.waitForEvent('dialog'); + await page.locator('#saveHistory').click(); + const dialog = await dialogPromise; + expect(dialog.message()).toBe('State already saved.'); + await dialog.accept(); + + await typeInEditor(page, ' C --> HistoryTest'); + await page.locator('#saveHistory').click(); + await expect(page.locator('#historyList li')).toHaveCount(2); + }); + + test.skip('should be able to restore and delete', async ({ page }) => { + await page.locator('#saveHistory').click(); + await typeInEditor(page, ' C --> HistoryTest'); + await expect(page.locator('#historyList').getByText('No items in History')).not.toBeVisible(); + await expect(page.locator('#historyList li')).toHaveCount(1); + await expect(page.locator('#view').getByText('HistoryTest')).toBeVisible(); + await page.getByText('Restore').click(); + await expect(page.locator('#view').getByText('HistoryTest')).not.toBeVisible(); + await page.getByText('Delete').click(); + await expect(page.locator('#historyList li')).toHaveCount(0); + await expect(page.locator('#historyList')).toContainText('No items in History'); + await page.locator('#saveHistory').click(); + await typeInEditor(page, ' C --> HistoryTest'); + await page.locator('#saveHistory').click(); + await page.locator('#editor').type('ing'); + await page.locator('#clearHistory').click(); + + const dialog = await page.waitForEvent('dialog'); + expect(dialog.message()).toBe('Clear all saved items?'); + await dialog.accept(); + + await expect(page.locator('#historyList')).toContainText('No items in History'); + }); +}); diff --git a/tests/loadSite.spec.ts b/tests/loadSite.spec.ts new file mode 100644 index 00000000..fff88111 --- /dev/null +++ b/tests/loadSite.spec.ts @@ -0,0 +1,113 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Site Loads', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await expect(page).toHaveURL(/.*\/edit#pako/); + }); + + test('Check Home page load', async ({ page }) => { + await expect(page).toHaveURL(/.*\/edit/); + await page.getByText('History').click(); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); + + test('should keep code after reload', async ({ page }) => { + await expect(page.locator('#editor')).toContainText('Car'); + await page.reload(); + await expect(page.locator('#editor')).toContainText('Car'); + }); + + test('Check Redirect from old URL', async ({ page }) => { + await page.goto( + '/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ' + ); + await expect(page).toHaveURL(/.*\/edit#pako:eNp/); + }); + + test('should load sample diagrams when clicked', async ({ page }) => { + await page.getByText('Sample Diagrams').click(); + await page.getByText('Pie').click(); + await expect(page.getByText('pie title Pets adopted by volunteers')).toBeVisible(); + await page.getByText('Class').click(); + await expect(page.getByText('classDiagram')).toBeVisible(); + }); + + test.describe('github', () => { + test('should load diagram from gist', async ({ page }) => { + await page.goto( + `/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a` + ); + await page.getByText('History').click(); + await expect(page.locator('#view').getByText('Go shopping!!')).toBeVisible(); + await expect(page.getByText('Revisions')).toBeVisible(); + await expect(page.getByText('sidharthv96 v8f8f1e2')).toBeVisible(); + await expect(page.getByText('sidharthv96 v7851e19')).toBeVisible(); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); + + test('should load diagram from gist revision', async ({ page }) => { + await page.goto( + '/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19' + ); + await page.getByText('History').click(); + await expect(page.locator('#view').getByText('Party')).toBeVisible(); + await expect(page.getByText('Revisions')).toBeVisible(); + await expect(page.getByText('sidharthv96 v7851e19')).toBeVisible(); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); + + test('should load diagram from raw files', async ({ page }) => { + await page.goto( + '/edit?code=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd&config=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json' + ); + await expect(page.locator('#view').getByText('Party')).toBeVisible(); + const codeStore = await page.evaluate(() => localStorage.getItem('codeStore')); + expect(codeStore).toBeTruthy(); + }); + }); + + test('should show troubleshooting steps if loading fails', async ({ page }) => { + await page.goto('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg'); + await page.reload({ waitUntil: 'networkidle' }); + await expect( + page.locator('#view').getByText('Please Click here to Raise an issue in github.') + ).toBeVisible(); + }); +}); + +test.describe('Verify types of URLs', () => { + test('should load compressed URL', async ({ page }) => { + await page.goto( + '/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ' + ); + await expect(page.locator('#view').getByText('New Year')).toBeVisible(); + await page.goto( + '/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ' + ); + await expect(page.locator('#view').getByText('Animal')).toBeVisible(); + }); + + test.describe('Uncompressed URLs', () => { + test('should load URL without specifier', async ({ page }) => { + await page.goto( + '/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ' + ); + await expect(page.locator('#view').getByText('TestLabel')).toBeVisible(); + await page.goto( + '/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ' + ); + await expect(page.locator('#view').getByText('Animal')).toBeVisible(); + }); + + test('should load URL with "base64" specifier', async ({ page }) => { + await page.goto( + '/edit/#base64:eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ' + ); + await expect(page.locator('#view').getByText('TestLabel')).toBeVisible(); + }); + }); +}); diff --git a/tests/themes.spec.ts b/tests/themes.spec.ts new file mode 100644 index 00000000..55c7f905 --- /dev/null +++ b/tests/themes.spec.ts @@ -0,0 +1,46 @@ +import { expect, test } from '@playwright/test'; + +test.describe('Test themes', () => { + test.describe('Test color scheme preference', () => { + test('should respect prefers-color-scheme light', async ({ page }) => { + // Test light mode + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto('/edit'); + await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/); + await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/); + }); + + test('should respect prefers-color-scheme dark', async ({ page }) => { + // Test dark mode + await page.emulateMedia({ colorScheme: 'dark' }); + await page.goto('/edit'); + await expect(page.getByText('dark').locator('..')).toHaveClass(/bordered/); + await expect(page.getByText('light').locator('..')).not.toHaveClass(/bordered/); + }); + }); + + test.describe('Test light themes', () => { + test.beforeEach(async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 768 }); + await page.goto('/edit'); + await page.getByText('Theme').click(); + }); + + test('should set light theme as default', async ({ page }) => { + await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/); + await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/); + const themeStore = await page.evaluate(() => localStorage.getItem('themeStore')); + expect(themeStore).toBeTruthy(); + }); + + test('should change themes when clicked', async ({ page }) => { + await expect(page.getByText('light').locator('..')).toHaveClass(/bordered/); + await page.getByText('cupcake').click(); + await expect(page.getByText('cupcake').locator('..')).toHaveClass(/bordered/); + await expect(page.getByText('light').locator('..')).not.toHaveClass(/bordered/); + await expect(page.getByText('dark').locator('..')).not.toHaveClass(/bordered/); + const themeStore = await page.evaluate(() => localStorage.getItem('themeStore')); + expect(themeStore).toBeTruthy(); + }); + }); +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 00000000..64340e15 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,61 @@ +import { type Page, expect } from '@playwright/test'; +import { readFileSync, statSync } from 'node:fs'; + +export const cmd = process.platform === 'darwin' ? 'Meta' : 'Control'; + +export interface EditorOptions { + bottom?: boolean; + newline?: boolean; +} + +interface WindowWithEditor { + editorLoaded: boolean; +} + +declare global { + interface Window extends WindowWithEditor {} +} + +export async function typeInEditor( + page: Page, + text: string, + { bottom = true, newline = false }: EditorOptions = {} +) { + await page.locator('#editor').click(); + const editor = page.locator('#editor'); + + if (bottom) { + await editor.locator('textarea').press('PageDown'); + } + if (newline) { + await editor.locator('textarea').press('Enter'); + } + await editor.locator('textarea').pressSequentially(text, { delay: 0 }); +} + +export async function verifyFileSizeGreaterThan( + page: Page, + fileType: 'history' | 'diagram', + extension: string, + size: number +) { + const download = await page.waitForEvent('download'); + const path = await download.path(); + if (!path) throw new Error('Download path not available'); + const fileSize = statSync(path).size; + expect(fileSize).toBeGreaterThan(size); + expect(fileSize).toBeLessThan(size * 2); +} + +export async function verifyFileSnapshot( + page: Page, + fileType: 'history' | 'diagram', + extension: string, + content: string +) { + const download = await page.waitForEvent('download'); + const path = await download.path(); + if (!path) throw new Error('Download path not available'); + const fileContent = readFileSync(path, 'utf8'); + expect(fileContent).toContain(content); +} diff --git a/tsconfig.json b/tsconfig.json index 91263b2e..3631886d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,13 +7,15 @@ "cypress/**/*.js", "cypress/**/*.json", "static/**/*.js", - "static/**/*.json" + "static/**/*.json", + "playwright.config.ts", + "tests/**/*.ts" ], "compilerOptions": { "resolveJsonModule": true, "allowSyntheticDefaultImports": true, "strictNullChecks": true, - "types": ["vitest/importMeta"] + "types": ["vitest/importMeta", "@playwright/test"] }, "extends": "./.svelte-kit/tsconfig.json" }