Each history entry now has a third action: a real anchor (rendered via the Button's href, target="_blank") linking to the editor with that entry's serialized state. Being a normal link, users can also copy it or open it in a new tab via the context menu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
148 lines
5.8 KiB
TypeScript
148 lines
5.8 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
|
|
const config = '{\n "theme": "default"\n}';
|
|
|
|
const entry = (id: string, name: string, type: 'manual' | 'auto', label: string) => ({
|
|
id,
|
|
name,
|
|
type,
|
|
time: Number(id.slice(2)),
|
|
state: {
|
|
code: `flowchart TD\n A[${label}]`,
|
|
mermaid: config,
|
|
autoSync: true,
|
|
updateDiagram: false
|
|
}
|
|
});
|
|
|
|
const manualHistory = [
|
|
entry('m-2', 'hollow-art', 'manual', 'Halloween'),
|
|
entry('m-1', 'helpful-ocean', 'manual', 'Pumpkin')
|
|
];
|
|
const autoHistory = [
|
|
entry('a-2', 'barking-dog', 'auto', 'NewYear'),
|
|
entry('a-1', 'needy-mosquito', 'auto', 'Fireworks')
|
|
];
|
|
|
|
const openHistory = (page: Page) => page.getByRole('button', { name: 'History' }).click();
|
|
|
|
test.describe('History', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Freeze time so auto-save snapshots are deterministic.
|
|
await page.addInitScript(() => {
|
|
Object.defineProperty(Date, 'now', { value: () => new Date(2022, 0, 1).getTime() });
|
|
});
|
|
await page.goto('/edit');
|
|
});
|
|
|
|
test('loads Saved and Timeline history from localStorage and restores entries', async ({
|
|
page
|
|
}) => {
|
|
await page.evaluate(
|
|
([manual, auto]) => {
|
|
localStorage.setItem('manualHistoryStore', manual);
|
|
localStorage.setItem('autoHistoryStore', auto);
|
|
},
|
|
[JSON.stringify(manualHistory), JSON.stringify(autoHistory)]
|
|
);
|
|
await page.reload();
|
|
await openHistory(page);
|
|
|
|
// Saved tab is active by default.
|
|
await expect(page.locator('#historyList li')).toHaveCount(2);
|
|
await expect(page.locator('#historyList')).toContainText('hollow-art');
|
|
await expect(page.locator('#historyList')).toContainText('helpful-ocean');
|
|
|
|
await page.getByRole('button', { name: 'Restore this version' }).first().click();
|
|
await expect(page.locator('#view')).toContainText('Halloween');
|
|
|
|
// Switching to the Timeline tab shows the auto entries only.
|
|
await page.getByRole('tab', { name: 'Timeline' }).click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(2);
|
|
await expect(page.locator('#historyList')).toContainText('barking-dog');
|
|
await expect(page.locator('#historyList')).toContainText('needy-mosquito');
|
|
await expect(page.locator('#historyList')).not.toContainText('hollow-art');
|
|
|
|
await page.getByRole('button', { name: 'Restore this version' }).first().click();
|
|
await expect(page.locator('#view')).toContainText('NewYear');
|
|
});
|
|
|
|
test('each entry has a copyable link that opens it in a new tab', async ({ page }) => {
|
|
await page.evaluate(
|
|
(manual) => localStorage.setItem('manualHistoryStore', manual),
|
|
JSON.stringify(manualHistory)
|
|
);
|
|
await page.reload();
|
|
await openHistory(page);
|
|
|
|
// It is a real link (so it can be copied / opened in a new tab), not a button.
|
|
const link = page.getByRole('link', { name: 'Open in new tab' }).first();
|
|
await expect(link).toHaveAttribute('target', '_blank');
|
|
const href = await link.getAttribute('href');
|
|
expect(href).toContain('/edit#pako:');
|
|
|
|
// Following it loads that entry's diagram.
|
|
await page.goto(href ?? '');
|
|
await expect(page.locator('#view')).toContainText('Halloween');
|
|
});
|
|
|
|
test('keeps the active tab highlighted when switching modes', async ({ page }) => {
|
|
await openHistory(page);
|
|
const saved = page.getByRole('tab', { name: 'Saved' });
|
|
const timeline = page.getByRole('tab', { name: 'Timeline' });
|
|
|
|
await expect(saved).toHaveClass(/border-b-2/);
|
|
await expect(timeline).not.toHaveClass(/border-b-2/);
|
|
|
|
await timeline.click();
|
|
await expect(timeline).toHaveClass(/border-b-2/);
|
|
await expect(saved).not.toHaveClass(/border-b-2/);
|
|
});
|
|
|
|
test('saves the current state and reports duplicates', async ({ page }) => {
|
|
await openHistory(page);
|
|
await expect(page.locator('#historyList li')).toHaveCount(0);
|
|
|
|
await page.locator('#saveHistory').click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(1);
|
|
|
|
// Saving again without changes does not add a duplicate and notifies the user.
|
|
await page.locator('#saveHistory').click();
|
|
await expect(page.getByText('State already saved.')).toBeVisible();
|
|
await expect(page.locator('#historyList li')).toHaveCount(1);
|
|
|
|
// Loading a different sample changes the state, so it saves as a new entry.
|
|
await page.getByRole('button', { name: 'Sequence', exact: true }).click();
|
|
await expect(page.locator('#view')).not.toContainText('Christmas');
|
|
await page.locator('#saveHistory').click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(2);
|
|
});
|
|
|
|
test('auto-saves to the Timeline only, never the Saved list', async ({ page }) => {
|
|
await openHistory(page);
|
|
await page.locator('#saveHistory').click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(1);
|
|
|
|
await page.getByRole('tab', { name: 'Timeline' }).click();
|
|
// A manual save must not appear under Timeline.
|
|
await expect(page.locator('#historyList')).toContainText('No timeline snapshots yet.');
|
|
});
|
|
|
|
test('deletes a single entry and clears all after confirmation', async ({ page }) => {
|
|
await openHistory(page);
|
|
await page.locator('#saveHistory').click();
|
|
await page.getByRole('button', { name: 'Sequence', exact: true }).click();
|
|
await expect(page.locator('#view')).not.toContainText('Christmas');
|
|
await page.locator('#saveHistory').click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(2);
|
|
|
|
await page.getByRole('button', { name: 'Delete this version' }).first().click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(1);
|
|
|
|
page.on('dialog', (dialog) => dialog.accept());
|
|
await page.locator('#clearHistory').click();
|
|
await expect(page.locator('#historyList li')).toHaveCount(0);
|
|
await expect(page.locator('#historyList')).toContainText('No saved states yet.');
|
|
});
|
|
});
|