major features update
This commit is contained in:
parent
519fb49901
commit
4e2fab67c5
48 changed files with 3925 additions and 208 deletions
57
e2e/Taskfile
Executable file
57
e2e/Taskfile
Executable file
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env bash
|
||||
PATH=$PATH:node_modules/.bin
|
||||
SCRIPT_DIR=$(cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd)
|
||||
|
||||
_playwright_version() {
|
||||
jq -r '.devDependencies["@playwright/test"]' "$SCRIPT_DIR/package.json" | sed -e 's/\^/v/'
|
||||
}
|
||||
|
||||
playwright:local() {
|
||||
exec docker run \
|
||||
--interactive --tty --rm --ipc=host --net=host \
|
||||
--volume "$SCRIPT_DIR":/e2e:rw --env BASE_URL="$BASE_URL" \
|
||||
--env ASTRO_TELEMETRY_DISABLED=1 \
|
||||
"mcr.microsoft.com/playwright:$(_playwright_version)" \
|
||||
bash -c "cd /e2e && env PROJECT_FILTER=firefox ./Taskfile _test $*"
|
||||
}
|
||||
|
||||
playwright:ui() {
|
||||
xhost +local:docker
|
||||
exec docker run \
|
||||
--interactive --tty --rm --ipc=host --net=host\
|
||||
--env DISPLAY="$DISPLAY" \
|
||||
--volume /tmp/.X11-unix:/tmp/.X11-unix \
|
||||
--volume "$SCRIPT_DIR":/e2e:rw --env BASE_URL="$BASE_URL" \
|
||||
--env ASTRO_TELEMETRY_DISABLED=1 \
|
||||
"mcr.microsoft.com/playwright:$(_playwright_version)" \
|
||||
/bin/bash -c "cd /e2e && ./Taskfile _test --ui $*"
|
||||
}
|
||||
|
||||
playwright:ci() {
|
||||
exec docker run \
|
||||
--interactive --tty --rm --ipc=host --net=host \
|
||||
--volume "$SCRIPT_DIR":/e2e:rw --env BASE_URL="$BASE_URL" \
|
||||
--env ASTRO_TELEMETRY_DISABLED=1 \
|
||||
"mcr.microsoft.com/playwright:$(_playwright_version)" \
|
||||
bash -c "cd /e2e && ./Taskfile _test $*"
|
||||
}
|
||||
|
||||
_test:full() {
|
||||
if ! test -f /.dockerenv; then
|
||||
echo "Don't run _test directly; use playwright:local."
|
||||
fi
|
||||
|
||||
# run only in firefox first to see if there's errors and,
|
||||
# only if not, run in everything else
|
||||
env PROJECT_FILTER=firefox playwright test "$@" && \
|
||||
env PROJECT_FILTER=!firefox playwright test "$@"
|
||||
}
|
||||
_test() {
|
||||
if ! test -f /.dockerenv; then
|
||||
echo "Don't run _test directly; use playwright:local."
|
||||
fi
|
||||
|
||||
playwright test "$@"
|
||||
}
|
||||
|
||||
"$@"
|
||||
16
e2e/package.json
Normal file
16
e2e/package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "mascarpone/e2e",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "playwright test --project=firefox && playwright test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@types/node": "^24.9.1"
|
||||
}
|
||||
}
|
||||
49
e2e/pages/home.spec.ts
Normal file
49
e2e/pages/home.spec.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { login, verifyCreateUser, todate } from './util';
|
||||
|
||||
test('can log out', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
await page.getByText("Logout").click();
|
||||
await expect(page.getByLabel("Username")).toBeVisible();
|
||||
});
|
||||
|
||||
test('has no contacts', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('can add contacts', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
||||
await expect(page.getByRole("navigation").getByRole("link")).toHaveCount(2);
|
||||
});
|
||||
|
||||
test('shows "never" for unfreshened contacts', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
|
||||
await expect(page.locator('#freshness')).toContainText('John Contactnever');
|
||||
});
|
||||
|
||||
test('shows the date for fresh contacts', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('button', { name: /fresh/i }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
await expect(page.locator('#freshness')).toContainText(`John Contact${todate()}`);
|
||||
});
|
||||
|
||||
test('sidebar is sorted alphabetically', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['Zulu'] });
|
||||
await verifyCreateUser(page, { names: ['Alfa'] });
|
||||
await verifyCreateUser(page, { names: ['Golf'] });
|
||||
|
||||
await expect(page.getByRole('navigation')).toHaveText(/Alfa\s*Golf\s*Zulu/);
|
||||
});
|
||||
37
e2e/pages/index.spec.ts
Normal file
37
e2e/pages/index.spec.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveTitle(/Mascarpone/);
|
||||
});
|
||||
|
||||
test('disbles submit button with empty fields', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Submit button should require both fields
|
||||
// to be non-empty
|
||||
await expect(page.getByRole("button")).toBeDisabled();
|
||||
|
||||
await page.getByLabel("Username").fill("bogus");
|
||||
await expect(page.getByRole("button")).toBeDisabled();
|
||||
|
||||
await page.getByLabel("Username").clear();
|
||||
await page.getByLabel("Password").fill("bogus");
|
||||
await expect(page.getByRole("button")).toBeDisabled();
|
||||
|
||||
await page.getByLabel("Username").fill("bogus");
|
||||
|
||||
await expect(page.getByRole("button")).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('has error message for invalid login', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await page.getByLabel("Username").fill("bogus");
|
||||
await page.getByLabel("Password").fill("bogus");
|
||||
|
||||
await page.getByRole("button").click();
|
||||
|
||||
await expect(page.getByText(/do not match/i)).toBeVisible();
|
||||
});
|
||||
117
e2e/pages/journal.spec.ts
Normal file
117
e2e/pages/journal.spec.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { login, verifyCreateUser, todate } from './util';
|
||||
|
||||
test('can add journal entries', async ({ page }) => {
|
||||
await login(page);
|
||||
|
||||
const entryBox = page.getByPlaceholder(/new entry/i);
|
||||
await entryBox.fill('banana banana banana');
|
||||
await page.getByRole('button', { name: /add entry/i }).click();
|
||||
|
||||
await expect(entryBox).toBeEmpty();
|
||||
await expect(page.getByText('banana banana banana')).toBeVisible();
|
||||
});
|
||||
|
||||
test('journal entries autolink', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
|
||||
await page.getByPlaceholder(/new entry/i).fill('met with [[John Contact]]');
|
||||
await page.getByRole('button', { name: /add entry/i }).click();
|
||||
|
||||
await expect(page.locator('#journal').getByRole('link', { name: 'John Contact' })).toBeVisible();
|
||||
});
|
||||
|
||||
test("changing a contact's names updates journal entries", async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await verifyCreateUser(page, { names: ['Jack Contact'] });
|
||||
|
||||
await page.getByPlaceholder(/new entry/i).fill('met with [[JC]]');
|
||||
await page.getByRole('button', { name: /add entry/i }).click();
|
||||
|
||||
const nav = page.getByRole('navigation');
|
||||
const journal = page.locator('#journal');
|
||||
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||
|
||||
// add a new name
|
||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
console.log(await journal.innerHTML());
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||
|
||||
// delete an existing name
|
||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('button', { name: '×', disabled: false }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||
|
||||
// put it back, then...
|
||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||
|
||||
// ...add a name that makes it no longer n=1
|
||||
await nav.getByRole("link", { name: 'Jack Contact' }).click();
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('textbox', { name: 'New name' }).fill('JC');
|
||||
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(0);
|
||||
|
||||
// delete a name that makes it now n=1
|
||||
await nav.getByRole("link", { name: 'John Contact' }).click();
|
||||
await page.getByRole('link', { name: /edit/i }).click();
|
||||
await page.getByRole('button', { name: '×', disabled: false }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
await expect.soft(journal.getByRole('link', { name: 'JC' })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('can edit existing journal entries on home page', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['John Contact'] });
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
|
||||
await page.getByPlaceholder(/new entry/i).fill("[[John Contact]]'s banana banana banana");
|
||||
await page.getByRole('button', { name: /add entry/i }).click();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await page.getByRole('checkbox', { name: /edit/i }).click();
|
||||
const textbox = page.locator('form').filter({
|
||||
has: page.getByRole('button', { name: '✓' })
|
||||
}).locator('textarea');
|
||||
await textbox.fill('met with [[John Contact]]');
|
||||
|
||||
await page.getByRole('button', { name: '✓' }).click();
|
||||
|
||||
await page.getByRole('checkbox', { name: /edit/i }).click();
|
||||
await expect(page.locator('#journal').getByRole('link', { name: 'John Contact' })).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('can have multiple links', async ({ page }) => {
|
||||
await login(page);
|
||||
await verifyCreateUser(page, { names: ['alice'] });
|
||||
await verifyCreateUser(page, { names: ['bob'] });
|
||||
await page.getByRole('link', { name: 'Mascarpone' }).click();
|
||||
|
||||
await page.getByPlaceholder(/new entry/i).fill('met with [[alice]] and [[bob]] and their kids');
|
||||
await page.getByRole('button', { name: /add entry/i }).click();
|
||||
|
||||
const journal = page.locator('#journal');
|
||||
await expect.soft(journal.getByRole('link', { name: 'alice' })).toHaveCount(1);
|
||||
await expect.soft(journal.getByRole('link', { name: 'bob' })).toHaveCount(1);
|
||||
});
|
||||
31
e2e/pages/util.ts
Normal file
31
e2e/pages/util.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Page } from '@playwright/test';
|
||||
|
||||
export const login = async (page: Page) => {
|
||||
await page.goto('/');
|
||||
await page.getByLabel("Username").fill("test");
|
||||
await page.getByLabel("Password").fill("test");
|
||||
await page.getByRole("button", { name: /login/i }).click();
|
||||
};
|
||||
|
||||
export const todate = () => new Date().toISOString().split('T')[0];
|
||||
|
||||
type UserFields = {
|
||||
names?: Array<string>,
|
||||
birthday?: string,
|
||||
};
|
||||
export const verifyCreateUser = async (page: Page, fields: UserFields) => {
|
||||
await page.getByRole('button', { name: /add contact/i }).click();
|
||||
|
||||
const { names, ...simple } = fields;
|
||||
for (const name of (names ?? [])) {
|
||||
await page.getByRole('textbox', { name: 'New name' }).fill(name);
|
||||
await page.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
}
|
||||
|
||||
for (const [label, value] of Object.entries(simple)) {
|
||||
await page.getByLabel(label).fill(value);
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
};
|
||||
|
||||
71
e2e/playwright.config.ts
Normal file
71
e2e/playwright.config.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
// purposefully not using ??: we want to replace empty empty string with default
|
||||
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
|
||||
|
||||
let addlConfig = {
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
};
|
||||
|
||||
let projects = [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
{
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
},
|
||||
];
|
||||
|
||||
const pfil = process.env.PROJECT_FILTER;
|
||||
if (pfil) {
|
||||
if (pfil.startsWith('!')) {
|
||||
projects = projects.filter(p => p.name !== pfil.slice(1));
|
||||
} else {
|
||||
projects = projects.filter(p => p.name === pfil);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './pages',
|
||||
fullyParallel: true,
|
||||
workers: 1,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: Boolean(process.env.CI),
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
baseURL: BASE_URL,
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects,
|
||||
...addlConfig,
|
||||
});
|
||||
|
||||
57
e2e/pnpm-lock.yaml
generated
Normal file
57
e2e/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.56.1
|
||||
version: 1.56.1
|
||||
'@types/node':
|
||||
specifier: ^24.9.1
|
||||
version: 24.9.1
|
||||
|
||||
packages:
|
||||
|
||||
/@playwright/test@1.56.1:
|
||||
resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
playwright: 1.56.1
|
||||
dev: true
|
||||
|
||||
/@types/node@24.9.1:
|
||||
resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==}
|
||||
dependencies:
|
||||
undici-types: 7.16.0
|
||||
dev: true
|
||||
|
||||
/fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/playwright-core@1.56.1:
|
||||
resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/playwright@1.56.1:
|
||||
resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
playwright-core: 1.56.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
dev: true
|
||||
|
||||
/undici-types@7.16.0:
|
||||
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
|
||||
dev: true
|
||||
BIN
e2e/users.db
Normal file
BIN
e2e/users.db
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue