Merge branch 'develop' into sidv/redesign

* develop:
  chore: Update script name
  chore: Remove cypress
  chore: Ignore cypress
  chore: Update test exclude
  chore: Tweak test config
  chore: Ignore cypress
  chore: Fix pw issues
  chore: Add playwright tests
This commit is contained in:
Sidharth Vinod
2025-03-18 12:59:32 -07:00
31 changed files with 665 additions and 2087 deletions
+20 -19
View File
@@ -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.51.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,20 @@ 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 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
+8 -5
View File
@@ -32,8 +32,11 @@ jobs:
node-version-file: '.node-version'
cache: 'pnpm'
- name: Lint & Test
run: |
pnpm install
pnpm lint
pnpm test:unit
- name: Install dependencies
run: pnpm install
- name: Lint
run: pnpm lint
- name: Run unit tests
run: pnpm test:unit
+6 -3
View File
@@ -5,10 +5,13 @@
/.svelte-kit
/build
/coverage
/cypress/downloads
/cypress/screenshots
/cypress/videos
/docs
/functions
/node_modules
/snapshots.js
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
-1
View File
@@ -1,4 +1,3 @@
[![Mermaid Live Editor](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/detailed/2ckppp/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/2ckppp/runs)
[![Join our Discord!](https://img.shields.io/static/v1?message=join%20chat&color=9cf&logo=discord&label=discord)](https://discord.gg/sKeNQX4Wtj)
[![Netlify Status](https://api.netlify.com/api/v1/badges/27fa023d-7c73-4a3f-9791-b3b657a47100/deploy-status)](https://app.netlify.com/sites/mermaidjs/deploys)
-37
View File
@@ -1,37 +0,0 @@
import { defineConfig } from 'cypress';
import fs from 'fs';
import path from 'path';
export default defineConfig({
projectId: '2ckppp',
viewportWidth: 1440,
viewportHeight: 768,
snapshotFileName: './cypress/snapshots.js',
defaultCommandTimeout: 5000,
requestTimeout: 5000,
retries: {
runMode: 2,
openMode: 0
},
e2e: {
setupNodeEvents(on, config) {
on('task', {
readAndDeleteFile({ fileNamePattern, folder, mode }) {
const fileNameRegex = new RegExp(fileNamePattern);
const files = fs.readdirSync(folder);
const filename = files.find((file) => file.match(fileNameRegex));
const filePath = path.join(folder, filename);
try {
if (mode === 'size') {
return fs.statSync(filePath).size;
}
return fs.readFileSync(filePath, 'utf8');
} finally {
fs.rmSync(filePath);
}
}
});
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.spec.ts'
}
});
-10
View File
@@ -1,10 +0,0 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": "off"
},
"env": {
"cypress/globals": true
}
}
-45
View File
@@ -1,45 +0,0 @@
import { typeInEditor, verifyFileSizeGreaterThan } from './util';
describe('Check actions', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Actions').click();
});
it('should update markdown code', () => {
cy.get('#markdown')
.invoke('val')
.then((oldText) => {
typeInEditor('C --> HistoryTest', { bottom: true, newline: true });
cy.get('#markdown')
.invoke('val')
.then((newText) => {
expect(oldText).to.not.eq(newText);
});
});
});
it.skip('should load gists from URL', () => {
cy.get('#gist').type('https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a');
cy.contains('Load Gist').click();
cy.contains('Go shopping!!');
});
it('should download png and svg', () => {
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 34_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 10_000);
// Verify downloaded file is different for different diagrams
cy.contains('Sample Diagrams').click();
cy.contains('ER').click();
cy.get(`#downloadPNG`).click();
verifyFileSizeGreaterThan('diagram', 'png', 35_000);
cy.get(`#downloadSVG`).click();
verifyFileSizeGreaterThan('diagram', 'svg', 11_000);
});
});
-113
View File
@@ -1,113 +0,0 @@
import { typeInEditor, cmd } from './util';
describe('Auto sync tests', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('contain', '/edit#pako');
cy.window().should('have.property', 'editorLoaded', true);
});
it('should dim diagram when code is edited', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#errorContainer').should('not.exist');
typeInEditor(' C --> Test', { bottom: true });
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
cy.getLocalStorage('codeStore').snapshot();
});
it('should update diagram when shortcut is used', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'Diagram out of sync.');
typeInEditor(`${cmd}{enter}`);
cy.get('#errorContainer').should('not.exist');
cy.get('#view').should('not.have.class', 'outOfSync');
});
it('should show/hide sync button with auto sync', () => {
cy.get('[data-cy=sync]').should('not.exist');
cy.contains('Auto sync').click();
cy.get('[data-cy=sync]').should('exist');
cy.get('#autoSync').check();
cy.get('[data-cy=sync]').should('not.exist');
});
it('should not dim diagram when code is in sync', () => {
cy.contains('Auto sync').click();
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(' C --> Test');
cy.get('#view').should('have.class', 'outOfSync');
cy.get('[data-cy=sync]').click();
cy.get('#view').should('not.have.class', 'outOfSync');
cy.get('#autoSync').check();
typeInEditor('ing');
cy.get('#view').should('not.have.class', 'outOfSync');
cy.getLocalStorage('codeStore').snapshot();
});
it('should automatically defer rendering when complex diagrams are edited', () => {
cy.get('#view').should('not.have.class', 'outOfSync');
typeInEditor(`
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i
A & B & C & D & E --> F & G & K & Z & i`);
cy.get('#view').should('have.class', 'outOfSync');
cy.get('#errorContainer').should('contain.text', 'It will be updated automatically.');
// The class should be removed automatically after 1 second.
cy.get('#view').should('not.have.class', 'outOfSync');
});
it('supports commenting code out/in', () => {
cy.get('#editor').contains('Car').click({ force: true });
cy.get('#editor').get('textarea').type(`${cmd}/`, { force: true });
cy.get('#view').contains('Car').should('not.exist');
typeInEditor(`{uparrow}${cmd}/`);
cy.get('#view').contains('Car').should('exist');
});
it('supports editing code when code is incorrect', () => {
cy.visit(
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
);
cy.contains('Diagram syntax error').should('not.exist');
typeInEditor(`branch test`, { bottom: true, newline: true });
cy.get('#editor').contains('branch test').should('exist');
cy.contains(
'Error: Trying to checkout branch which is not yet created. (Help try using "branch master")'
).should('exist');
});
it('should update diagram after entire text is removed', () => {
// https://github.com/mermaid-js/mermaid-live-editor/issues/1102
typeInEditor(`${cmd} a {backspace}`);
typeInEditor('graph LR');
typeInEditor(' {enter} A-->Car');
cy.get('#view').contains('Car').should('exist');
});
});
describe('Pan and Zoom', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
});
it('should toggle pan and zoom', () => {
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('exist');
cy.contains('Pan & Zoom').click();
cy.get('#svg-pan-zoom-reset-pan-zoom').should('not.exist');
});
});
-36
View File
@@ -1,36 +0,0 @@
describe('Editor docs tests', () => {
beforeEach(() => {
cy.on('uncaught:exception', () => {
return false;
});
cy.clearLocalStorage();
cy.visit('/edit');
cy.contains('Sample Diagrams').click();
});
it('Test default loading', () => {
cy.get(`[data-cy=docs][href^="https://mermaid.js.org/"]`).should('exist');
});
it('Test to see if the correct URL loads when changing from one diagram to other', () => {
cy.contains('Flow').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart.html"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/flowchart.html#configuration"]`).should('exist');
cy.contains('Sequence').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram.html#configuration"]`).should('exist');
cy.contains('Code').click();
cy.get(`[data-cy=docs][href$="/syntax/sequenceDiagram.html"]`).should('exist');
});
it("Test to check URLs for a case where config URL doesn't exist", () => {
cy.contains('State').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram.html"]`).should('exist');
cy.contains('Config').click();
cy.get(`[data-cy=docs][href$="/syntax/stateDiagram.html"]`).should('exist');
});
});
-106
View File
@@ -1,106 +0,0 @@
import { typeInEditor, verifyFileSnapshot } from './util';
describe('Save History', () => {
beforeEach(() => {
cy.clock(new Date(2022, 0, 1).getTime());
cy.clearLocalStorage();
cy.visit('/edit');
cy.on('uncaught:exception', (error) => {
// Skip the error from inside monaco.
return !error.message.includes('duration');
});
cy.contains('History').click();
});
afterEach(() => {
cy.clock().invoke('restore');
});
it('should load history from localstorage', () => {
cy.setLocalStorage(
'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"}]'
);
cy.setLocalStorage(
'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"}]'
);
cy.reload();
cy.contains('History').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('helpful-ocean');
cy.get('#historyList').contains('hollow-art');
cy.contains('Restore').click();
cy.contains('Halloween');
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 2);
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').contains('needy-mosquito');
cy.get('#historyList').contains('barking-dog');
cy.contains('Restore').click();
cy.contains('New Year');
});
it('should save when clicked', () => {
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#saveHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('State already saved.');
});
cy.on('window:confirm', () => true);
typeInEditor(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#historyList').find('li').should('have.length', 2);
});
it('should be able to restore and delete', () => {
cy.get('#saveHistory').click();
typeInEditor(' C --> HistoryTest');
cy.get('#historyList').find('No items in History').should('not.exist');
cy.get('#historyList').find('li').should('have.length', 1);
cy.contains('HistoryTest');
cy.contains('Restore').click();
cy.contains('HistoryTest').should('not.exist');
cy.contains('Delete').click();
cy.get('#historyList').find('li').should('have.length', 0);
cy.get('#historyList').contains('No items in History');
cy.get('#saveHistory').click();
typeInEditor(' C --> HistoryTest');
cy.get('#saveHistory').click();
cy.get('#editor').type('ing');
cy.get('#clearHistory').click();
cy.on('window:alert', (str) => {
expect(str).to.equal('Clear all saved items?');
});
cy.on('window:confirm', () => true);
cy.get('#historyList').contains('No items in History');
});
// TODO: Fix #639
xit('should auto save history', () => {
typeInEditor(' C --> HistoryTest');
cy.tick(70_000);
cy.contains('Timeline').click();
cy.get('#historyList').find('li').should('have.length', 1);
cy.get('#editor').type('ing');
cy.tick(70_000);
cy.get('#historyList').find('li').should('have.length', 2);
for (let i = 0; i < 31; i++) {
cy.get('#editor').type('.');
cy.tick(70_000);
}
cy.get('#historyList').find('li').should('have.length', 30);
});
it('should download history', () => {
cy.get('#saveHistory').click();
cy.get(`#downloadHistory`).click();
verifyFileSnapshot('history', 'json', 'A[Christmas] -->|Get money| B(Go shopping)');
});
});
-136
View File
@@ -1,136 +0,0 @@
import { toBase64 } from 'js-base64';
describe('Site Loads', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/');
cy.url().should('include', '/edit#pako');
});
it('Check Home page load', () => {
cy.url().should('include', '/edit');
cy.contains('History').click();
cy.getLocalStorage('codeStore').snapshot();
});
it('should keep code after reload', () => {
cy.get('#editor').contains('Car');
cy.reload();
cy.get('#editor').contains('Car');
});
it('Check Redirect from old URL', () => {
cy.visit(
'/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7InRoZW1lIjoiZGVmYXVsdCJ9LCJ1cGRhdGVFZGl0b3IiOmZhbHNlfQ'
);
cy.url().should('include', '/edit#pako:eNp');
});
it('should load sample diagrams when clicked', () => {
cy.contains('Sample Diagrams').click();
cy.contains('Pie').click();
cy.contains('pie title Pets adopted by volunteers');
cy.contains('Class').click();
cy.contains('classDiagram');
});
(Cypress.env('CI') === 'true' ? describe : describe.skip)('github', () => {
it('should load diagram from gist', () => {
cy.visit(`/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a`);
cy.contains('History').click();
cy.contains('Go shopping!!');
cy.contains('Revisions');
cy.contains('sidharthv96 v8f8f1e2');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from gist revision', () => {
cy.visit(
'/edit?gist=https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19'
);
cy.contains('History').click();
cy.contains('Party');
cy.contains('Revisions');
cy.contains('sidharthv96 v7851e19');
cy.getLocalStorage('codeStore').snapshot();
});
it('should load diagram from raw files', () => {
cy.visit(
'/edit?code=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd&config=https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json'
);
cy.contains('Party');
cy.getLocalStorage('codeStore').snapshot();
});
});
// Disabled temporarily. Should be enabled after the issue is fixed in Mermaid.
// it('should prevent setting the "securityLevel" option via URL', () => {
// const b64State = toBase64(
// `{"code":"graph TD\\nA[\\"<img src='https://via.placeholder.com/64' width=64 />\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`,
// true
// );
// cy.on('window:confirm', () => true);
// cy.visit(`/edit#${b64State}`);
// cy.contains('Config').click();
// cy.contains('forest');
// cy.contains('securityLevel').should('not.exist');
// cy.get('#view').find('img').should('not.exist');
// cy.get('#view').contains('<img');
// cy.get('#view').contains(`src='https://via.placeholder.com/64'`);
// });
it.skip('should allow persisting "securityLevel" using confirm dialogue', () => {
const b64State = toBase64(
`{"code":"graph TD\\nA[\\"<img src='https://dummyimage.com/64' width=64/>\\"]","mermaid":"{\\"securityLevel\\": \\"loose\\", \\"theme\\": \\"forest\\"}","autoSync":true,"updateDiagram":true}`,
true
);
cy.on('window:confirm', () => false);
cy.visit(`/edit#${b64State}`);
cy.get('#editor').type(' ');
cy.contains('Config').click();
cy.contains('forest');
cy.contains('securityLevel');
cy.get('#view').find('img').should('be.visible');
});
it('should show troubleshooting steps if loading fails', () => {
cy.visit('/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAg');
cy.reload(true);
cy.contains('Please Click here to Raise an issue in github.');
});
});
describe('Verify types of URLs', () => {
it('should load compressed URL', () => {
cy.visit(
'/edit#pako:eNpVkM2KwkAQhF-l6dMK5gVyEDRxvYi7sF6WjIcm0zqDzg_jBJEk725Hd2G3Tw31VVFUj23QjCWeEkUD-1p5kFs2O77BN1M6QFEshg1ncMHzfYDV2ybA1YQYrT_NXvhqgqDqtxPGkI315_ElVU__h-cB6mZLMYd4-Kvsb2GAdWM_jcT_V0xicb03RyqPVLSUoJI-OEfHyZHV0rqfDAqzYccKS3k1pbNC5Ufhuqgp81rbHBJKxuXKc6Quh6-7b7HMqeNfqLYkC7gfanwAlW1ZvQ'
);
cy.contains('New Year');
cy.visit(
'/edit#pako:eNptkU1PwzAMhv9K5BOI9Q9EXBDbJA477YYqITcxndV8QD40weh_Jy1rGR0-OY_tV2_sEyivCSQogzGuGduAtnaixINji0bcf1WVWGfVXdMtx8M1faYm4B8sxR27JLClJd6nwK4VLTlN4bI4jMQd2pLe3C4KFhNNcLQ92jv9ADGLNoTdozc-zIV4ZDsNlud7RtVN7_5Sb_jYrFcN3iN_0pPbEqUZK3QbTP_Ojyv4NdR4bwTHlyMbPcOQ3WJ2CliBpWCRdbnLqFJDOpClGmRJNYauhtr1pS-_6bKMjebkA8hXNJFWgDn5_YdTIFPINDWdb3vu6r8BaWOZRQ'
);
cy.contains('Animal');
});
describe('Uncompressed URLs', () => {
it('should load URL without specifier', () => {
cy.visit(
'/edit/#eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
cy.visit(
'/edit#eyJjb2RlIjoiY2xhc3NEaWFncmFtXG4gICAgQW5pbWFsIDx8LS0gRHVja1xuICAgIEFuaW1hbCA8fC0tIEZpc2hcbiAgICBBbmltYWwgPHwtLSBaZWJyYVxuICAgIEFuaW1hbCA6ICtpbnQgYWdlXG4gICAgQW5pbWFsIDogK1N0cmluZyBnZW5kZXJcbiAgICBBbmltYWw6ICtpc01hbW1hbCgpXG4gICAgQW5pbWFsOiArbWF0ZSgpXG4gICAgY2xhc3MgRHVja3tcbiAgICAgICtTdHJpbmcgYmVha0NvbG9yXG4gICAgICArc3dpbSgpXG4gICAgICArcXVhY2soKVxuICAgIH1cbiAgICBjbGFzcyBGaXNoe1xuICAgICAgLWludCBzaXplSW5GZWV0XG4gICAgICAtY2FuRWF0KClcbiAgICB9XG4gICAgY2xhc3MgWmVicmF7XG4gICAgICArYm9vbCBpc193aWxkXG4gICAgICArcnVuKClcbiAgICB9XG4gICAgICAgICAgICAiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGFya1wiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('Animal');
});
it('should load URL with "base64" specifier', () => {
cy.visit(
'/edit/#base64:eyJjb2RlIjoiZ3JhcGhcbiAgICBUZXN0TGFiZWwiLCJtZXJtYWlkIjoie1xuICBcInRoZW1lXCI6IFwiZGVmYXVsdFwiXG59IiwidXBkYXRlRWRpdG9yIjpmYWxzZSwiYXV0b1N5bmMiOnRydWUsInVwZGF0ZURpYWdyYW0iOmZhbHNlfQ'
);
cy.contains('TestLabel');
});
});
});
-56
View File
@@ -1,56 +0,0 @@
describe('Test themes', () => {
describe('Test light themes', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: false
});
}
});
cy.contains('Theme').click();
});
it('should set light theme as default', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
it('should change themes when clicked', () => {
cy.contains('light').parent().should('have.class', 'bordered');
cy.contains('cupcake').click();
cy.contains('cupcake').parent().should('have.class', 'bordered');
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('not.have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
describe('Test dark mode', () => {
beforeEach(() => {
cy.clearLocalStorage();
cy.visit('/edit', {
onBeforeLoad(win) {
cy.stub(win, 'matchMedia')
.callThrough()
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: true
});
}
});
cy.contains('Theme').click();
});
it('should set dark theme as default', () => {
cy.contains('light').parent().should('not.have.class', 'bordered');
cy.contains('dark').parent().should('have.class', 'bordered');
cy.getLocalStorage('themeStore').snapshot();
});
});
});
-55
View File
@@ -1,55 +0,0 @@
export const cmd = `{${Cypress.platform === 'darwin' ? 'meta' : 'ctrl'}}`;
interface EditorOptions {
bottom?: boolean;
newline?: boolean;
}
export const typeInEditor = (
text: string,
{ bottom = true, newline = false }: EditorOptions = {}
) => {
cy.window().should('have.property', 'editorLoaded', true);
cy.get('#editor').click();
cy.get('#editor').within(() => {
if (bottom) {
cy.get('textarea').type('{pageDown}');
}
if (newline) {
cy.get('textarea').type('{enter}');
}
cy.get('textarea').type(text);
});
};
const downloadsFolder = Cypress.config('downloadsFolder');
export const verifyFileSizeGreaterThan = (
fileType: 'history' | 'diagram',
extension: string,
size: number
) => {
cy.get('#view').should('not.have.class', 'outOfSync');
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'size'
}).then((fileSize: number) => {
expect(fileSize).to.be.gt(size);
expect(fileSize).to.be.lt(size * 2);
});
};
export const verifyFileSnapshot = (
fileType: 'history' | 'diagram',
extension: string,
content: string
) => {
cy.task('readAndDeleteFile', {
folder: downloadsFolder,
fileNamePattern: `^mermaid-${fileType}-.*.${extension}$`,
mode: 'content'
}).then((fileContent: number) => {
expect(fileContent).to.contain(content);
});
};
-5
View File
@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
-52
View File
@@ -1,52 +0,0 @@
module.exports = {
"Site Loads": {
"Check Home page load": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"rough\":false,\"updateDiagram\":true,\"panZoom\":false}"
},
"Check Redirect from old URL": {
"1": "{\"code\":\"graph TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true}"
},
"should load diagram from gist": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a\"}}}"
},
"should load diagram from gist revision": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"gist\",\"config\":{\"url\":\"https://gist.github.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/ec9b4ab0e41e4ff6287326cd3cb47affd7851e19\"}}}"
},
"should load diagram from raw files": {
"1": "{\"code\":\"graph TD\\n A[Party] -->|Get money| B(Go shopping!!)\\n \",\"mermaid\":\"{\\n \\\"theme\\\": \\\"forest\\\",\\n \\\"test\\\": \\\"hello world\\\"\\n}\",\"autoSync\":true,\"updateDiagram\":true,\"loader\":{\"type\":\"files\",\"config\":{\"codeURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/code.mmd\",\"configURL\":\"https://gist.githubusercontent.com/sidharthv96/6268a23e673a533dcb198f241fd7012a/raw/4eb03887e6a41397e80bdcdbf94017c498f8f1e2/config.json\"}}}"
}
},
"__version": "12.17.4",
"Auto sync tests": {
"should dim diagram when code is edited": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Test\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":false,\"rough\":false,\"updateDiagram\":false,\"panZoom\":false}"
},
"should not dim diagram when code is in sync": {
"1": "{\"code\":\"flowchart TD\\n A[Christmas] -->|Get money| B(Go shopping)\\n B --> C{Let me think}\\n C -->|One| D[Laptop]\\n C -->|Two| E[iPhone]\\n C -->|Three| F[fa:fa-car Car]\\n C --> Testing\",\"mermaid\":\"{\\n \\\"theme\\\": \\\"default\\\"\\n}\",\"autoSync\":true,\"rough\":false,\"updateDiagram\":false,\"panZoom\":false}"
}
},
"Test themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
},
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
},
"Test light themes": {
"should set light theme as default": {
"1": "{\"theme\":\"light\",\"isDark\":false}"
},
"should change themes when clicked": {
"1": "{\"theme\":\"cupcake\",\"isDark\":false}"
}
},
"Test dark mode": {
"should set dark theme as default": {
"1": "{\"theme\":\"dark\",\"isDark\":true}"
}
}
}
}
-38
View File
@@ -1,38 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
import { register } from '@cypress/snapshot';
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
register();
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
snapshot(): void;
}
}
}
import 'cypress-localstorage-commands';
-27
View File
@@ -1,27 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.on('uncaught:exception', (err) => {
/* returning false here prevents Cypress from failing the test */
if (err.message.includes('ResizeObserver loop limit exceeded')) {
return false;
}
});
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"types": ["cypress", "cypress-localstorage-commands", "node"]
},
"include": ["**/*.ts"]
}
+8 -12
View File
@@ -15,14 +15,14 @@
"pre-commit": "lint-staged",
"postinstall": "husky install && svelte-kit sync && (git config blame.ignoreRevsFile .git-blame-ignore-revs || true)",
"test:unit": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage",
"test:browser": "cypress run",
"test": "pnpm test:unit && pnpm test:browser",
"cy": "cypress open"
"test:unit:ui": "vitest --ui",
"test:unit:coverage": "vitest run --coverage",
"test": "pnpm test:unit && pnpm test:e2e",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
},
"devDependencies": {
"@cypress/snapshot": "2.1.7",
"@fortawesome/fontawesome-free": "^6.5.2",
"@iconify-json/material-symbols": "^1.2.15",
"@iconify-json/mdi": "^1.2.3",
@@ -42,11 +42,8 @@
"chai": "^4.3.10",
"clsx": "^2.1.1",
"cssnano": "^6.0.5",
"cypress": "12.17.4",
"cypress-localstorage-commands": "2.2.7",
"eslint": "8.57.1",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-cypress": "2.15.2",
"eslint-plugin-es": "^4.1.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-postcss-modules": "^2.0.0",
@@ -79,7 +76,8 @@
"unplugin-icons": "^22.1.0",
"vite": "^5.4.14",
"vitest": "^2.1.9",
"vitest-dom": "^0.1.1"
"vitest-dom": "^0.1.1",
"@playwright/test": "^1.42.1"
},
"dependencies": {
"@fontsource-variable/recursive": "^5.2.5",
@@ -110,8 +108,6 @@
"packageManager": "pnpm@10.6.4+sha512.da3d715bfd22a9a105e6e8088cfc7826699332ded60c423b14ec613a185f1602206702ff0fe4c438cb15c979081ce4cb02568e364b15174503a63c7a8e2a5f6c",
"pnpm": {
"onlyBuiltDependencies": [
"@cypress/snapshot",
"cypress",
"deasync",
"esbuild",
"svelte-preprocess"
+28
View File
@@ -0,0 +1,28 @@
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 ? 3 : undefined,
reporter: process.env.CI ? [['github'], ['list']] : 'list',
use: {
baseURL: 'http://localhost:3000',
browserName: 'chromium',
permissions: ['clipboard-read', 'clipboard-write'],
trace: 'retain-on-failure',
viewport: { width: 1920, height: 1080 }
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
}
],
webServer: {
command: `pnpm ${process.env.CI ? 'preview' : 'dev'}`,
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
}
});
+58 -1306
View File
File diff suppressed because it is too large Load Diff
-13
View File
@@ -1,12 +1,3 @@
<script lang="ts" module>
declare global {
interface Window {
Cypress: boolean;
editorLoaded: boolean;
}
}
</script>
<script lang="ts">
import McWrapper from '$/components/McWrapper.svelte';
import MermaidChartIcon from '$/components/MermaidChartIcon.svelte';
@@ -109,10 +100,6 @@
resizeObserver.observe(divElement);
}
if (window.Cypress) {
window.editorLoaded = true;
}
return () => {
unsubscribeState();
unsubscribeMode();
+51
View File
@@ -0,0 +1,51 @@
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();
const firstPngSize = await downloadPNGPromise;
const downloadSVGPromise = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 10_000);
await page.locator('#downloadSVG').click();
const firstSvgSize = 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();
const secondPngSize = await downloadPNGPromise2;
const downloadSVGPromise2 = verifyFileSizeGreaterThan(page, 'diagram', 'svg', 11_000);
await page.locator('#downloadSVG').click();
const secondSvgSize = await downloadSVGPromise2;
// Verify files are actually different
expect(firstPngSize).not.toBe(secondPngSize);
expect(firstSvgSize).not.toBe(secondSvgSize);
});
});
+127
View File
@@ -0,0 +1,127 @@
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('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('Control+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('should automatically defer rendering when complex diagrams are edited', async ({
page
}) => {
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/);
await typeInEditor(
page,
`
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K
A & B & C & D & E --> F & G & H & I & J & K`
);
await expect(page.locator('#view')).toHaveClass(/outOfSync/);
await expect(page.locator('#errorContainer')).toContainText(
'It will be updated automatically.'
);
// The class should be removed automatically after 1 second
await expect(page.locator('#view')).not.toHaveClass(/outOfSync/, { timeout: 2000 });
});
test('supports commenting code out/in', async ({ page }) => {
await page.locator('#editor').getByText('fa-car').click();
await page.waitForTimeout(1000);
await page.keyboard.press('Control+/');
await expect(page.locator('#view').getByText('Car')).not.toBeVisible();
await page.keyboard.press('Control+/');
await expect(page.locator('#view').getByText('Car')).toBeVisible();
});
test('supports editing code when code is incorrect', async ({ page }) => {
await page.goto(
'/edit#pako:eNpljjEKwzAMRa8SNOcEnlt6gK5eVFvYJsgOqkwpIXevg9smEE1PnyfxF3DFExgISW-CczQ2D21cYU7a-SGYXRwyvTp9jUhuKlVP-eHy7zA-leQsMEmg_QOM0BLG5FujZVMsaCQmC6ahR5ks2Lw2r84ela4-aREwKpVGwKrl_s7ut3fnkjAIcg_XDzuaUhs'
);
await expect(page.getByText('Diagram syntax error')).not.toBeVisible();
await typeInEditor(page, 'branch test', { bottom: true, newline: true });
await expect(page.locator('#editor')).toContainText('branch test');
await expect(
page.getByText('Error: Trying to checkout branch which is not yet created.')
).toBeVisible();
});
test('should update diagram after entire text is removed', async ({ page }) => {
await page.locator('#editor').click();
await page.keyboard.press('Control+KeyA');
await page.keyboard.press('Backspace');
await typeInEditor(
page,
`graph LR
A-->Bike`
);
await expect(page.locator('#view').getByText('Bike')).toBeVisible();
});
});
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();
});
});
+42
View File
@@ -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();
});
});
+84
View File
@@ -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');
});
});
+113
View File
@@ -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();
});
});
});
+46
View File
@@ -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();
});
});
});
+62
View File
@@ -0,0 +1,62 @@
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 page.keyboard.type(text, { delay: 10 });
}
export async function verifyFileSizeGreaterThan(
page: Page,
fileType: 'history' | 'diagram',
extension: string,
size: number
): Promise<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);
return fileSize;
}
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);
}
+4 -5
View File
@@ -3,17 +3,16 @@
"src/**/*.d.ts",
"src/**/*.ts",
"src/**/*.svelte",
"cypress/**/*.ts",
"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"
}
+8
View File
@@ -36,6 +36,14 @@ export default defineConfig({
environment: 'jsdom',
// in-source testing
includeSource: ['src/**/*.{js,ts,svelte}'],
// Ignore E2E tests
exclude: [
'tests/**/*',
'**\/node_modules/**',
'**\/dist/**',
'**\/.{idea,git,cache,output,temp}/**',
'**\/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*'
],
setupFiles: ['./src/tests/setup.ts'],
coverage: {
exclude: ['src/mocks', '.svelte-kit', 'src/**/*.test.ts'],