major features update

This commit is contained in:
Robert Perce 2025-11-27 13:45:21 -06:00
parent 519fb49901
commit 4e2fab67c5
48 changed files with 3925 additions and 208 deletions

49
e2e/pages/home.spec.ts Normal file
View 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
View 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
View 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
View 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();
};