Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions dembrane/frontend/e2e/flows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,15 +314,22 @@ export const flows: Record<string, Flow> = {
},
"/w/:workspaceId/projects/new": {
params: { workspaceId: "$workspace" },
// The template picker, then the name step with "More options" open (ECHO-1042).
// The template picker by purpose: a verb filter, all cards, then the name step with
// "What this sets up" and "More options" open (ECHO-1042, ECHO-1053).
states: [
{ clicks: ["template-filter-verb-decide"], name: "filtered" },
{ clicks: ["template-filter-show-all"], name: "all templates" },
{
clicks: ["template-card-event", "create-project-template-continue"],
clicks: [
"template-card-resident_evening",
"create-project-template-continue",
"template-setup-summary-toggle",
],
name: "name",
},
{
clicks: [
"template-card-event",
"template-card-resident_evening",
"create-project-template-continue",
"create-project-more-options-toggle",
],
Expand Down
179 changes: 156 additions & 23 deletions dembrane/frontend/e2e/templates-harness.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { expect, type Page, test } from "@playwright/test";
import { RESIDENT_EVENING_GUIDE } from "../src/components/project/templates/hostGuide.fixture";

// ECHO-1042: the template picker, the name step with "More options", and workspace home's
// first-project state, at desktop and phone width. E2E_SHOTS=<dir> also saves screenshots.
// ECHO-1042, ECHO-1053: the template picker by purpose (most used first, each verb filter,
// the setup filter), the name step with "What this sets up" open, the portal editor's name
// box label, a host guide a template filled in, and workspace home's first-project state,
// at desktop and phone width. E2E_SHOTS=<dir> also saves screenshots.
const shots = process.env.E2E_SHOTS;
const sizes = [
{ height: 900, name: "desktop", width: 1280 },
{ height: 844, name: "phone", width: 390 },
];
const VERBS = ["gather", "understand", "explore", "align", "decide"];

async function answerApi(page: Page) {
const guide = (nl: boolean) =>
nl ? RESIDENT_EVENING_GUIDE.nl : RESIDENT_EVENING_GUIDE.en;

async function answerApi(page: Page, nl: boolean) {
await page.route("**/v2/workspaces/w1/projects?**", (route) =>
route.fulfill({
json: {
Expand Down Expand Up @@ -37,56 +44,182 @@ async function answerApi(page: Page) {
await page.route("**/v2/workspaces/w1/free-allowance**", (route) =>
route.fulfill({ json: { free: false } }),
);
await page.route("**/v2/projects/p1/bff**", (route) =>
route.fulfill({
json: {
// The portal editor's legal basis section reads the same endpoint.
_legal: {
effective: {
legal_basis: "client-managed",
privacy_policy_url: null,
source: "system",
},
inherited: {
legal_basis: "client-managed",
privacy_policy_url: null,
source: "system",
},
organiser_name: null,
},
default_conversation_ask_for_participant_name: true,
host_guide: guide(nl),
id: "p1",
is_conversation_allowed: true,
language: nl ? "nl" : "en",
legal_basis: null,
name: nl ? "Bewonersavond Lindenwijk" : "Lindenwijk resident evening",
privacy_policy_url: null,
},
}),
);
await page.route("**/conversations**", (route) =>
route.fulfill({ json: [] }),
);
}

for (const size of sizes)
for (const lang of ["en-US", "nl-NL"])
test(`templates harness, ${size.name}, ${lang}`, async ({ page }) => {
test.setTimeout(90_000);
const nl = lang === "nl-NL";
const errors: string[] = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.setViewportSize({ height: size.height, width: size.width });
await page.addInitScript((l) => {
localStorage.setItem("dembrane-language", l);
}, lang);
await answerApi(page);
await answerApi(page, nl);
const shot = async (name: string) => {
if (shots)
await page.screenshot({
fullPage: true,
path: `${shots}/${size.name}-${lang}-${name}.png`,
});
};
const noSideScroll = async () =>
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);

await page.goto(
`/e2e/templates-harness.html?path=/${lang}/w/w1/projects/new`,
);
await expect(page.getByTestId("template-picker")).toBeVisible();
await expect(page.getByRole("radio")).toHaveCount(4);
await page.getByTestId("template-card-event").click();
await expect(page.getByTestId("template-card-event")).toHaveAttribute(
"aria-checked",
"true",
);
await shot("1-picker");
// No sideways scroll at phone width.
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
// The first view: the most used six, and a resident evening among them.
await expect(page.getByRole("radio")).toHaveCount(6);
await expect(
page.getByTestId("template-card-resident_evening"),
).toContainText(nl ? "Bewonersavond" : "Resident evening");
// Continue waits for a card, and says so.
await expect(
page.getByTestId("create-project-template-continue"),
).toBeDisabled();
await shot("01-first-view");
await noSideScroll();

for (const verb of VERBS) {
await page.getByTestId(`template-filter-verb-${verb}`).click();
await expect(
page.getByTestId(`template-filter-verb-${verb}`),
).toHaveAttribute("aria-pressed", "true");
await shot(`02-verb-${verb}`);
}
// The last verb pressed again clears it.
await page.getByTestId("template-filter-verb-decide").click();
await page.getByTestId("template-filter-setup-many_groups").click();
await expect(page.getByRole("radio")).toHaveCount(10);
await shot("03-many-groups");
await noSideScroll();

await page.getByTestId("create-project-template-continue").click();
await page.getByTestId("template-card-resident_evening").click();
await expect(
page.getByTestId("template-card-resident_evening"),
).toHaveAttribute("aria-checked", "true");
// The bug from the earlier screenshot: Continue must be enabled and look it.
const next = page.getByTestId("create-project-template-continue");
await expect(next).toBeEnabled();
expect(
await next.evaluate((el) => getComputedStyle(el).backgroundColor),
).not.toBe("rgba(0, 0, 0, 0)");
await shot("04-selected");
await next.click();
await expect(
page.getByTestId("create-project-chosen-template"),
).toBeVisible();
await shot("2-name");
await page.getByTestId("create-project-more-options-toggle").click();
await expect(page.getByTestId("create-project-review")).toBeVisible();
await page.getByTestId("template-setup-summary-toggle").click();
await expect(
page.getByTestId("template-setup-summary-script"),
).toHaveText(
nl
? "Een tafel leiden op de bewonersavond"
: "Hosting a table at a resident evening",
);
await page.waitForTimeout(300);
await shot("3-more-options");
await shot("05-name-what-this-sets-up");
await noSideScroll();

await page.goto(`/e2e/templates-harness.html?path=/${lang}/editor`);
await expect(
page.getByTestId("portal-editor-name-label-input"),
).toHaveValue(
nl ? "Naam of nummer van je tafel" : "Table name or number",
);
await page
.getByTestId("portal-editor-name-label-input")
.evaluate((el) => el.scrollIntoView({ block: "center" }));
if (shots)
await page.screenshot({
path: `${shots}/${size.name}-${lang}-06-editor-name-label.png`,
});

await page.goto(
`/e2e/templates-harness.html?path=/${lang}/w/w1/projects/p1/host-guide`,
);
await expect(
page.getByText(
nl
? "Een tafel leiden op de bewonersavond"
: "Hosting a table at a resident evening",
),
).toBeVisible();
await page.waitForTimeout(300);
await shot("07-host-guide");

// The portal's name box: the example follows the label (ECHO-1053).
for (const [name, label, placeholder] of [
[
"09-portal-name-default",
"",
nl ? "Groep 1, Tafel 3, enz." : "Group 1, Table 3, etc.",
],
[
"09-portal-name-table",
nl ? "Naam of nummer van je tafel" : "Table name or number",
null,
],
[
"09-portal-name-interview",
nl
? "Interviewcode (geen echte namen)"
: "Interview code (no real names)",
null,
],
] as const) {
await page.goto(
`/e2e/templates-harness.html?path=/${lang}/p1/start&label=${encodeURIComponent(label)}`,
);
const input = page.getByTestId("portal-initiate-name-input");
await expect(input).toBeVisible();
expect(await input.getAttribute("placeholder")).toBe(placeholder);
await shot(name);
}

await page.goto(`/e2e/templates-harness.html?path=/${lang}/w/w1/home`);
await expect(page.getByTestId("workspace-home-start")).toBeVisible();
await shot("4-workspace-home");
await expect(page.getByTestId("template-picker")).toBeVisible();
await shot("08-workspace-home");
await noSideScroll();
expect(errors).toEqual([]);
});
7 changes: 7 additions & 0 deletions dembrane/frontend/lingui.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const config: LinguiConfig = {
exclude: [
"<rootDir>/src/features/accounts/**",
"<rootDir>/src/lib/errors/messages/**",
"<rootDir>/src/components/project/templates/**",
],
include: ["src"],
path: "<rootDir>/src/locales/{locale}",
Expand All @@ -19,6 +20,12 @@ const config: LinguiConfig = {
include: ["<rootDir>/src/features/accounts"],
path: "<rootDir>/src/features/accounts/locales/{locale}",
},
{
// The template picker's own catalog (ECHO-1053): sixteen templates' names, lines
// and facts, loaded when the picker opens (src/components/project/templates/i18n.ts).
include: ["<rootDir>/src/components/project/templates"],
path: "<rootDir>/src/components/project/templates/locales/{locale}",
},
{
// The friendly message for every error code, loaded with the first error a
// screen shows (src/lib/errors/present.ts), never in the first load.
Expand Down
2 changes: 2 additions & 0 deletions dembrane/frontend/src/components/common/settingChange.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { Text } from "@mantine/core";
export const FIELD_LABELS: Record<string, () => string> = {
anonymize_transcripts: () => t`Anonymise transcripts`,
context: () => t`Project context`,
conversation_ask_for_participant_name_label: () =>
t`What the name box asks for`,
conversation_title_prompt: () => t`Title guidance`,
default_conversation_ask_for_participant_email: () =>
t`Ask participants for their email`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,31 @@ it("leaves the Continue button alone when the project asks for a name", () => {
expect(screen.getByTestId("portal-initiate-next-button")).toBeTruthy();
});

it("gives the default name label a neutral example, with no person's name", () => {
wrap(project({ default_conversation_ask_for_participant_name: true }));
const input = screen.getByTestId("portal-initiate-name-input");
expect(input.getAttribute("placeholder")).toBe("Group 1, Table 3, etc.");
});

it("leaves the example out when the host set the label", () => {
for (const label of [
"Table name or number",
"Meeting name or date",
"Interview code (no real names)",
]) {
wrap(
project({
conversation_ask_for_participant_name_label: label,
default_conversation_ask_for_participant_name: true,
}),
);
expect(screen.getByText(label)).toBeTruthy();
const input = screen.getByTestId("portal-initiate-name-input");
expect(input.getAttribute("placeholder")).toBeNull();
cleanup();
}
});

const refusal = {
response: {
data: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ export const ParticipantInitiateForm = ({
onRefusedChange?.(Boolean(refused));
}, [refused, onRefusedChange]);

const customNameLabel =
project.conversation_ask_for_participant_name_label?.trim() || "";

return (
<form
onSubmit={handleSubmit(startConversation)}
Expand All @@ -285,11 +288,14 @@ export const ParticipantInitiateForm = ({
// autoFocus
required
size="md"
label={
project.conversation_ask_for_participant_name_label ??
t`Session name`
label={customNameLabel || t`Session name`}
// The example follows the label. A host's own label (a table, a
// meeting, an interview code) says what to type, so it gets none: an
// example of a person's name would invite one. The default label
// gets a neutral example.
placeholder={
customNameLabel ? undefined : t`Group 1, Table 3, etc.`
}
placeholder="Group 1, John Doe, etc."
{...register("name")}
error={errors.name?.message}
className="w-full"
Expand Down
Loading
Loading