Files
crumbs-original/tests/e2e/settings.spec.ts
T
Frederic CabassutandClaude Opus 4.6 370b0bc815 fix: eliminate e2e test flakiness in settings preference and API key tests
Three sources of flakiness fixed:

1. **Preferences sync race** — page.goto() triggers a full reload which
   re-runs initPreferences() + syncPreferencesFromServer(). On slow CI,
   the async server sync can return stale data from parallel test workers
   that share the same user's server-side preferences. Fixed with
   gotoWithStablePrefs() which blocks the one GET /api/preferences sync
   request, so initPreferences() falls back to localStorage (always correct).

2. **localStorage not reset** — resetPreferences() only reset the server.
   After a full page reload, loadFromLocalStorage() could return stale
   values from previous test runs. Now resets both server and localStorage.

3. **API key name collision** — parallel test runs created keys with the
   same name ("Settings Test Key"), causing Playwright strict-mode
   violations when multiple elements matched. Fixed with Date.now()
   unique suffixes.

Verified: 167/167 settings tests pass (20x repeat), 173/173 full suite (5x).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 23:39:29 +01:00

206 lines
7.0 KiB
TypeScript

import { test, expect } from './helpers/fixtures.js';
import type { Page } from '@playwright/test';
const DEFAULT_PREFS = {
defaultNoteMode: 'richtext',
defaultNoteColor: 'default',
hideFooter: false,
sidebarDefaultState: 'open'
};
/**
* Reset preferences to defaults on both server and client localStorage.
*
* Server reset ensures GET /api/preferences returns defaults.
* localStorage reset ensures initPreferences() picks up correct values
* even if the sync request is blocked or returns stale data.
*/
async function resetPreferences(page: Page) {
await page.request.put('/api/preferences', {
data: {
defaultNoteMode: 'richtext',
defaultNoteColor: 'default',
hideFooter: 'false',
sidebarDefaultState: 'open'
}
});
await page.evaluate(
(prefs) => localStorage.setItem('crumbs-preferences', JSON.stringify(prefs)),
DEFAULT_PREFS
);
}
/**
* Navigate to a URL with reliable preference state.
*
* page.goto() triggers a full page reload which re-runs initPreferences():
* 1. loadFromLocalStorage() — synchronous, returns correct value
* 2. syncPreferencesFromServer() — async, can return stale data from
* parallel test workers that share the same user's server-side prefs
*
* This helper blocks the one GET /api/preferences sync request so
* initPreferences() falls back to localStorage (which is always correct
* from either resetPreferences or updatePreference on the settings page).
*/
async function gotoWithStablePrefs(page: Page, url: string) {
await page.route(
'**/api/preferences',
(route) => route.abort(),
{ times: 1 }
);
await page.goto(url);
}
test.describe('Settings — Preferences', () => {
test('Scenario: Preferences tab is visible in settings nav', async ({
authenticatedPage: page
}) => {
// When the user navigates to settings
await page.goto('/settings');
// Then the Preferences nav link is visible
await expect(page.getByTestId('settings-nav-preferences')).toBeVisible();
});
test('Scenario: Default note mode change applies to new notes', async ({
authenticatedPage: page
}) => {
await resetPreferences(page);
// Given the user sets default note mode to Markdown
await gotoWithStablePrefs(page, '/settings/preferences');
const putResponse = page.waitForResponse((res) => res.url().includes('/api/preferences') && res.request().method() === 'PUT');
await page.getByTestId('pref-mode-markdown').click();
await putResponse;
// When the user creates a new note
await gotoWithStablePrefs(page, '/');
await page.getByTestId('new-note-btn').click();
// Then the note editor opens in markdown mode (textarea visible)
await expect(page.getByTestId('note-content-input')).toBeVisible();
});
test('Scenario: Footer toggle hides and shows footer', async ({
authenticatedPage: page
}) => {
await resetPreferences(page);
// Given the footer is visible
await gotoWithStablePrefs(page, '/');
await expect(page.getByTestId('app-footer')).toBeVisible();
// When the user enables the hide footer preference
await gotoWithStablePrefs(page, '/settings/preferences');
await page.getByTestId('pref-hide-footer').check();
// Then the footer is hidden
await gotoWithStablePrefs(page, '/');
await expect(page.getByTestId('app-footer')).not.toBeVisible();
// When the user disables the hide footer preference
await gotoWithStablePrefs(page, '/settings/preferences');
await page.getByTestId('pref-hide-footer').uncheck();
// Then the footer is visible again
await gotoWithStablePrefs(page, '/');
await expect(page.getByTestId('app-footer')).toBeVisible();
});
test('Scenario: Preferences persist across page reload', async ({
authenticatedPage: page
}) => {
await resetPreferences(page);
// Given the user changes default note mode to Markdown
await gotoWithStablePrefs(page, '/settings/preferences');
const putResponse = page.waitForResponse((res) => res.url().includes('/api/preferences') && res.request().method() === 'PUT');
await page.getByTestId('pref-mode-markdown').click();
await putResponse;
// When the page is reloaded (block sync to prevent parallel-test interference)
await page.route('**/api/preferences', (route) => route.abort(), { times: 1 });
await page.reload();
// Then the Markdown button is still selected (has primary styling)
const mdBtn = page.getByTestId('pref-mode-markdown');
await expect(mdBtn).toHaveClass(/font-medium/);
});
});
test.describe('Settings — API Key Management', () => {
test('Scenario: Created API key appears in the keys list', async ({
authenticatedPage: page
}) => {
// Use a unique name to avoid strict-mode violations from parallel test runs
const keyName = `Test Key ${Date.now()}`;
// When the user navigates to the MCP settings and creates an API key
await page.goto('/settings/mcp');
await page.getByTestId('api-key-name-input').fill(keyName);
await page.getByTestId('create-api-key-btn').click();
// Then the key is shown once for copying
await expect(page.getByTestId('created-key-display')).toBeVisible();
const keyValue = await page.getByTestId('created-key-value').textContent();
expect(keyValue).toMatch(/^crumbs_/);
// And it appears in the keys list
const keyItem = page.getByTestId('api-key-item').filter({ hasText: keyName });
await expect(keyItem).toBeVisible();
});
test('Scenario: Revoked API key disappears from the keys list', async ({
authenticatedPage: page
}) => {
const keyName = `Revoke Key ${Date.now()}`;
// Given an API key exists
await page.goto('/settings/mcp');
await page.getByTestId('api-key-name-input').fill(keyName);
await page.getByTestId('create-api-key-btn').click();
await expect(page.getByTestId('created-key-display')).toBeVisible();
const keyItem = page.getByTestId('api-key-item').filter({ hasText: keyName });
await expect(keyItem).toBeVisible();
// When the user revokes the key
await keyItem.getByTestId('delete-api-key-btn').click();
await keyItem.getByTestId('confirm-delete-btn').click();
// Then the key is removed from the list
await expect(keyItem).not.toBeVisible();
});
test('Scenario: Sidebar settings link opens the preferences page', async ({
authenticatedPage: page
}) => {
// When the user navigates to settings from the sidebar
await page.getByTestId('settings-link').click();
// Then the preferences page is displayed
await expect(page).toHaveURL('/settings/preferences');
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
});
test('Scenario: Settings tabs navigate between subpages', async ({
authenticatedPage: page
}) => {
// Given the user is on the settings page
await page.goto('/settings');
// When the user selects the API tab
await page.getByRole('link', { name: 'API' }).click();
// Then the API settings are displayed
await expect(page).toHaveURL('/settings/mcp');
await expect(page.getByRole('heading', { name: 'API Keys' })).toBeVisible();
// When the user selects the Profile tab
await page.getByRole('link', { name: 'Profile' }).click();
// Then the profile settings are displayed
await expect(page).toHaveURL('/settings/profile');
});
});