Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.
The real-group profiles stay as a test, so a change that moves a pick fails. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
96ca15d8e3
commit
bcbc6f30d9
89 files changed
+36555
-2828
No files matched your search
@@ -35,7 +35,7 @@ async function declineDiscardCustomizeEdits() {
|
||||
/**
|
||||
* Mounts the screen with optional starting state and exposes the latest
|
||||
* `state` to the test harness so we can assert the persistence side of
|
||||
* the Add Platform flow without driving the wizard's Next chain.
|
||||
* the Add card flow without driving the wizard's Next chain.
|
||||
*/
|
||||
const EMPTY_STATE: CreateFlowState = {};
|
||||
|
||||
@@ -62,7 +62,7 @@ function ScreenWithStateProbe({
|
||||
* snapping back to preset copy. See {@link CommunicationMethodEditFields}
|
||||
* and `buildPublishPayload` for the read side.
|
||||
*/
|
||||
describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
describe("CommunicationMethodsScreen — Add card persistence", () => {
|
||||
it("starts empty and Generate draft inserts the catalog preset", async () => {
|
||||
let latest: CreateFlowState = {};
|
||||
render(
|
||||
@@ -74,7 +74,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
@@ -96,7 +96,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
fireEvent.change(corePrincipleField, { target: { value: "Custom principle" } });
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Add Platform",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
void dialog;
|
||||
@@ -157,7 +157,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
void latest;
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textareas = within(dialog).getAllByRole(
|
||||
@@ -190,7 +190,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textareas = within(dialog).getAllByRole(
|
||||
@@ -259,7 +259,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textareas = within(dialog).getAllByRole(
|
||||
@@ -283,7 +283,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
@@ -312,7 +312,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
@@ -344,7 +344,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textboxes = within(dialog).getAllByRole(
|
||||
@@ -387,7 +387,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textboxes = within(dialog).getAllByRole(
|
||||
@@ -448,7 +448,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
await screen.findByText("Custom policy details"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Core Principle & Scope" }),
|
||||
screen.getByRole("button", { name: "Core principle and scope" }),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Finalize policy" }));
|
||||
|
||||
@@ -470,7 +470,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
for (const button of within(dialog).getAllByRole("button", {
|
||||
@@ -490,11 +490,11 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
await screen.findByText("Custom policy details"),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(
|
||||
screen.getByRole("button", { name: "Core Principle & Scope" }),
|
||||
screen.getByRole("button", { name: "Core principle and scope" }),
|
||||
);
|
||||
expect(await screen.findByText("Add text block")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByDisplayValue(/We prioritize privacy and security/i),
|
||||
screen.getByDisplayValue(/Signal encrypts every message/i),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Back" }));
|
||||
@@ -526,7 +526,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const cardDialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(
|
||||
@@ -585,9 +585,9 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||
: "";
|
||||
});
|
||||
expect(labels[0]).toMatch(/Logistics, Admin/);
|
||||
expect(labels[1]).toMatch(/Code of Conduct/);
|
||||
expect(labels[2]).toMatch(/Core Principle/);
|
||||
expect(labels[0]).toMatch(/How it works/);
|
||||
expect(labels[1]).toMatch(/Norms/);
|
||||
expect(labels[2]).toMatch(/Core principle/);
|
||||
});
|
||||
|
||||
it("does not block tab close when the create modal is unchanged", async () => {
|
||||
@@ -600,7 +600,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
await screen.findByRole("dialog");
|
||||
expect(dispatchBeforeUnload()).toBe(false);
|
||||
@@ -616,7 +616,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
const textboxes = within(dialog).getAllByRole(
|
||||
@@ -636,7 +636,7 @@ describe("CommunicationMethodsScreen — Add Platform persistence", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", { name: /Signal: Encrypted messaging/ })[0],
|
||||
screen.getAllByRole("button", { name: /Signal: We coordinate in encrypted Signal groups/ })[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
fireEvent.click(within(dialog).getByRole("button", { name: "More options" }));
|
||||
|
||||
@@ -21,7 +21,7 @@ describe("CommunityStructureSelectScreen", () => {
|
||||
renderWithProviders(<CommunityStructureSelectScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Worker cooperative" }),
|
||||
screen.getByRole("button", { name: "Select Worker co-op" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Local" }),
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("ConflictManagementScreen", () => {
|
||||
render(<ConflictManagementScreen />);
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", {
|
||||
name: /Peer Mediation: Trained members/,
|
||||
name: /Conflict ladder: We talk one-on-one/,
|
||||
})[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
fireEvent.click(
|
||||
within(dialog).getAllByRole("button", { name: "Generate draft" })[0],
|
||||
);
|
||||
expect(fields[0].value).toMatch(/everyone can use/i);
|
||||
expect(fields[0].value).toMatch(/usable by everyone/i);
|
||||
});
|
||||
|
||||
it("closes a pending value without confirm when fields are unchanged", async () => {
|
||||
@@ -408,7 +408,7 @@ describe("CoreValuesSelectScreen", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("five-value limit", () => {
|
||||
describe("six-value limit", () => {
|
||||
async function addPresetValue(label: string) {
|
||||
fireEvent.click(screen.getByText(label));
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
@@ -420,22 +420,23 @@ describe("CoreValuesSelectScreen", () => {
|
||||
|
||||
it("shows a counter, disables remaining chips, and announces the limit", async () => {
|
||||
renderWithProviders(<CoreValuesSelectScreen />);
|
||||
expect(screen.getByText("0 of 5")).toBeInTheDocument();
|
||||
expect(screen.getByText("0 of 6")).toBeInTheDocument();
|
||||
|
||||
await addPresetValue("Accessibility");
|
||||
await addPresetValue("Accountability");
|
||||
await addPresetValue("Adaptability");
|
||||
await addPresetValue("Agency");
|
||||
await addPresetValue("Altruism");
|
||||
await addPresetValue("Anti-oppression");
|
||||
await addPresetValue("Autonomy");
|
||||
|
||||
expect(screen.getByText("5 of 5")).toBeInTheDocument();
|
||||
expect(screen.getByText("6 of 6")).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Five of five values selected. Remaining values are unavailable until one is removed.",
|
||||
"Six of six values selected. Remaining values are unavailable until one is removed.",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Select Anti-oppression" }),
|
||||
screen.getByRole("button", { name: "Select Capacity building" }),
|
||||
).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "Add value" })).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: /^add$/i })).toBeDisabled();
|
||||
|
||||
@@ -205,9 +205,9 @@ function FinalReviewWithCustomizeSelections() {
|
||||
{ id: "2", label: "Accountability", state: "unselected" },
|
||||
],
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
@@ -220,16 +220,39 @@ describe("FinalReviewScreen — prefilled selections", () => {
|
||||
expect(screen.getByText("Accessibility")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Signal")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open Access")).toBeInTheDocument();
|
||||
expect(screen.getByText("Lazy Consensus")).toBeInTheDocument();
|
||||
expect(screen.getByText("Peer Mediation")).toBeInTheDocument();
|
||||
expect(screen.getByText("Open admission")).toBeInTheDocument();
|
||||
expect(screen.getByText("Lazy consensus")).toBeInTheDocument();
|
||||
expect(screen.getByText("Conflict ladder")).toBeInTheDocument();
|
||||
|
||||
// Demo chips from `finalReview.json` must not leak through once the
|
||||
// user has real selections: "Open Admission" is shipped as fallback,
|
||||
// while the customize flow resolves to "Open Access".
|
||||
// user has real selections. The fallback still says "Open Admission".
|
||||
expect(screen.queryByText("Open Admission")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Consciousness")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("warns when binding arbitration sits next to a join that records no yes", async () => {
|
||||
function ReviewWithOpenJoinAndArbitration() {
|
||||
const { replaceState } = useCreateFlow();
|
||||
useLayoutEffect(() => {
|
||||
replaceState({
|
||||
title: "Oak Park Commons",
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedConflictManagementIds: ["binding-arbitration"],
|
||||
});
|
||||
}, [replaceState]);
|
||||
return <FinalReviewScreen />;
|
||||
}
|
||||
|
||||
render(<ReviewWithOpenJoinAndArbitration />);
|
||||
expect(
|
||||
await screen.findByText(/Binding arbitration needs each member.s own yes/),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Each member still has to agree separately before binding arbitration applies/,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("FinalReviewScreen — chip detail modal", () => {
|
||||
@@ -245,7 +268,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Encrypted messaging for high-security, private coordination\./i,
|
||||
/encrypted Signal groups/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -277,7 +300,7 @@ describe("FinalReviewScreen — chip detail modal", () => {
|
||||
);
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole("button", { name: catalogChipName("Video Meetings") }),
|
||||
await screen.findByRole("button", { name: catalogChipName("Video meetings") }),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(within(dialog).getAllByRole("textbox")).toHaveLength(3);
|
||||
@@ -1021,9 +1044,9 @@ describe("FinalReviewScreen — chip edit modal save semantics", () => {
|
||||
? (document.getElementById(labelledby)?.textContent ?? "").trim()
|
||||
: "";
|
||||
});
|
||||
expect(labels[0]).toMatch(/Logistics, Admin/);
|
||||
expect(labels[1]).toMatch(/Code of Conduct/);
|
||||
expect(labels[2]).toMatch(/Core Principle/);
|
||||
expect(labels[0]).toMatch(/How it works/);
|
||||
expect(labels[1]).toMatch(/Norms/);
|
||||
expect(labels[2]).toMatch(/Core principle/);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("MembershipMethodsScreen", () => {
|
||||
render(<MembershipMethodsScreen />);
|
||||
fireEvent.click(
|
||||
screen.getAllByRole("button", {
|
||||
name: /Open Access: Maximum inclusion/,
|
||||
name: /Open admission: Anyone who agrees/,
|
||||
})[0],
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
|
||||
@@ -48,7 +48,7 @@ test.describe("Critical User Journeys", () => {
|
||||
await page.locator("text=Consensus").first().click();
|
||||
await page.locator("text=Do-ocracy").first().click();
|
||||
await page.locator("text=Devolution").first().click();
|
||||
await page.locator("text=Quadratic Governance").first().click();
|
||||
await page.locator("text=Solidarity Network").first().click();
|
||||
|
||||
// 7. User checks out features
|
||||
const features = [
|
||||
|
||||
Vendored
+27880
File diff suppressed because it is too large.
Load diff
Vendored
+194
@@ -0,0 +1,194 @@
|
||||
# How far each card reaches
|
||||
|
||||
Made by engine/run-profiles.ts. Counts are out of the 92 real-group profiles as given, 43 mutual aid groups and 49 worker co-ops and close relatives. Recommended counts every recommended card, which has no cap, and Shown counts the cards in the seven. With fixes gives both counts after the marks in _marksToConfirm.json, only where they change.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Job | Tier | Card | Recommended | Shown | With fixes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Everyday decisions | frame | Decision speeds (every Rule) | 92 | 92 | |
|
||||
| Everyday decisions | frame | Two-person sign-off | 0 | 0 | |
|
||||
| Everyday decisions | frame | Do-ocracy | 0 | 0 | |
|
||||
| Everyday decisions | frame | Advice process | 0 | 0 | |
|
||||
| Big decisions | fitted | Lazy consensus | 31 | 31 | |
|
||||
| Big decisions | fitted | Modified consensus | 13 | 13 | |
|
||||
| Big decisions | fitted | Weighted voting | 0 | 0 | |
|
||||
| Big decisions | fitted | General meeting | 5 | 5 | |
|
||||
| Big decisions | fitted | Hierarchical decision-making | 2 | 2 | |
|
||||
| Big decisions | fitted | One person decides | 7 | 7 | |
|
||||
| Big decisions | fitted | Consensus decision-making | 4 | 4 | |
|
||||
| Big decisions | fitted | Consent (sociocracy) | 28 | 28 | |
|
||||
| Big decisions | fitted | Majority rule | 1 | 1 | |
|
||||
| Big decisions | fitted | Supermajority rule | 0 | 0 | |
|
||||
| Big decisions | fitted | Consensus seeking with delegates | 1 | 1 | |
|
||||
| Changing this Rule | floor | Changing this Rule (every Rule) | 92 | 92 | |
|
||||
| Changing this Rule | floor | Petition and referendum | 0 | 0 | |
|
||||
| Changing this Rule | floor | Closing the group | 0 | 0 | |
|
||||
| Emergencies | safety | Emergency decisions (every Rule) | 92 | 92 | |
|
||||
| Emergencies | safety | On-site lead (added) | 43 | 43 | |
|
||||
| Money | floor | Shared money (every Rule) | 75 | 56 | |
|
||||
| Money | floor | Annual budget | 17 | 12 | |
|
||||
| Money | floor | Pay and hiring (added) | 39 | 37 | |
|
||||
| Money | floor | Decide by formula | 0 | 0 | |
|
||||
| Money | floor | Participatory budgeting | 0 | 0 | |
|
||||
| Money | floor | How we are covered (added) | 14 | 14 | |
|
||||
| Spending | floor | Spending limits | 75 | 17 | |
|
||||
| Spending | floor | No card | 17 | | |
|
||||
| Roles and delegation | fitted | Elected board of directors | 12 | 12 | |
|
||||
| Roles and delegation | fitted | Delegated decision-making | 69 | 69 | |
|
||||
| Roles and delegation | fitted | Rotating roles | 7 | 7 | |
|
||||
| Roles and delegation | fitted | Decisions about one person | 4 | 4 | |
|
||||
| Roles and delegation | fitted | Follow-through | 0 | 0 | |
|
||||
| Roles and delegation | fitted | Coordinating committee | 0 | 0 | |
|
||||
| Roles and delegation | fitted | Appointed board | 0 | 0 | |
|
||||
| Roles and delegation | fitted | Conflicts of interest | 0 | 0 | |
|
||||
| Roles and delegation | fitted | Ranked choice voting | 0 | 0 | |
|
||||
| Roles and delegation | fitted | Panel drawn by lot | 0 | 0 | |
|
||||
|
||||
63 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 6 to 10.
|
||||
|
||||
## Membership
|
||||
|
||||
| Job | Tier | Card | Recommended | Shown | With fixes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Joining | frame | Open admission | 43 | 43 | |
|
||||
| Joining | frame | Worker-owner candidacy | 47 | 47 | |
|
||||
| Joining | frame | Join by contributing | 0 | 0 | |
|
||||
| Joining | frame | Membership by token | 0 | 0 | |
|
||||
| Joining | frame | One person one account | 0 | 0 | |
|
||||
| Joining | frame | Members approve new members | 0 | 0 | |
|
||||
| Joining | frame | Application and review | 2 | 2 | |
|
||||
| Joining | frame | Vouching | 0 | 0 | |
|
||||
| Joining | frame | Membership agreement | 0 | 0 | |
|
||||
| Joining | frame | Member share or dues | 0 | 0 | |
|
||||
| Joining | frame | Member groups (added) | 18 | 18 | |
|
||||
| Joining | frame | Common bond | 0 | 0 | |
|
||||
| Joining | frame | Lottery for limited places | 0 | 0 | |
|
||||
| Levels of membership | fitted | Volunteers and core members | 24 | 24 | |
|
||||
| Levels of membership | fitted | Contributors and maintainers | 0 | 0 | |
|
||||
| Levels of membership | fitted | Equal membership | 56 | 56 | 16 and 16 |
|
||||
| Levels of membership | fitted | Starting with one person | 0 | 0 | |
|
||||
| Levels of membership | fitted | Owners and staff | 2 | 2 | 42 and 42 |
|
||||
| Levels of membership | fitted | Paid staff | 4 | 4 | |
|
||||
| Levels of membership | fitted | Trial period | 6 | 6 | |
|
||||
| Levels of membership | fitted | Requirements for some roles | 0 | 0 | |
|
||||
| Levels of membership | fitted | Member classes | 0 | 0 | |
|
||||
| Levels of membership | fitted | A voice for people without a vote (added) | 31 | 31 | |
|
||||
| Learning how we work | fitted | Companions | 39 | 39 | |
|
||||
| Learning how we work | fitted | Joining in crews | 46 | 46 | |
|
||||
| Learning how we work | fitted | Orientation | 7 | 7 | |
|
||||
| Staying and leaving | fitted | Rest and workload limits | 23 | 23 | |
|
||||
| Staying and leaving | fitted | Leave and return | 32 | 32 | |
|
||||
| Staying and leaving | fitted | Leaving the group | 35 | 35 | |
|
||||
| Staying and leaving | fitted | Handing over roles | 2 | 2 | |
|
||||
| Staying and leaving | fitted | Roster upkeep | 0 | 0 | |
|
||||
| Removal | safety | Ending a membership (every Rule) | 92 | 92 | |
|
||||
|
||||
0 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 5 to 7.
|
||||
|
||||
## Conflict
|
||||
|
||||
| Job | Tier | Card | Recommended | Shown | With fixes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| First steps | frame | Conflict ladder (every Rule) | 92 | 92 | |
|
||||
| Safety first | safety | Precautionary suspension (every Rule) | 92 | 92 | |
|
||||
| Safety first | safety | Safety lead | 0 | 0 | |
|
||||
| Safety first | safety | Working with children | 0 | 0 | |
|
||||
| Who hears serious cases | fitted | Conflict working group | 62 | 62 | |
|
||||
| Who hears serious cases | fitted | Mediation | 11 | 11 | |
|
||||
| Who hears serious cases | fitted | Code of conduct team | 0 | 0 | |
|
||||
| Who hears serious cases | fitted | Hearing panel | 17 | 17 | |
|
||||
| Who hears serious cases | fitted | Managerial decision | 2 | 2 | |
|
||||
| Repair and removal | floor | Graduated responses (every Rule) | 92 | 92 | |
|
||||
| Repair and removal | floor | Restorative practices | 0 | 0 | |
|
||||
| Repair and removal | floor | Accountability process | 0 | 0 | |
|
||||
| Outside help and legal duties | safety | Harassment and discrimination complaints | 55 | 55 | |
|
||||
| Outside help and legal duties | safety | Outside help | 37 | 37 | |
|
||||
| Outside help and legal duties | safety | Whistleblower protection | 0 | 0 | |
|
||||
| Legal matters | floor | Legal action (every Rule) | 92 | 0 | |
|
||||
| Legal matters | floor | Disputes with partners | 0 | 0 | |
|
||||
| Legal matters | floor | Non-binding arbitration | 0 | 0 | |
|
||||
| Legal matters | floor | Binding arbitration | 0 | 0 | |
|
||||
| Police and arrests | safety | Police and immigration visits | 92 | 92 | |
|
||||
| Police and arrests | safety | Legal support for members | 0 | 0 | |
|
||||
| Prevention | fitted | Regular debriefs | 92 | 92 | |
|
||||
| Prevention | fitted | Conflict workshops | 0 | 0 | |
|
||||
| Prevention | fitted | Circle processes | 0 | 0 | |
|
||||
|
||||
92 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 8 to 8.
|
||||
|
||||
## Communication
|
||||
|
||||
| Job | Tier | Card | Recommended | Shown | With fixes |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Day-to-day chat | frame | Signal | 85 | 85 | |
|
||||
| Day-to-day chat | frame | Slack | 7 | 7 | |
|
||||
| Day-to-day chat | frame | Matrix / Element | 0 | 0 | |
|
||||
| Day-to-day chat | frame | Discord | 0 | 0 | |
|
||||
| Day-to-day chat | frame | WhatsApp | 0 | 0 | |
|
||||
| Day-to-day chat | frame | Chat channels | 0 | 0 | |
|
||||
| Day-to-day chat | frame | Field communication | 0 | 0 | |
|
||||
| Day-to-day chat | frame | Telegram | 0 | 0 | |
|
||||
| Day-to-day chat | frame | Zulip | 0 | 0 | |
|
||||
| Our records | floor | Shared records (every Rule) | 92 | 92 | |
|
||||
| Our records | floor | GitHub / GitLab | 0 | 0 | |
|
||||
| Records about people | safety | Records about people | 90 | 90 | |
|
||||
| Records about people | safety | No card | 2 | | |
|
||||
| Reaching every member | floor | Announcements channel (every Rule) | 92 | 92 | |
|
||||
| Reaching every member | floor | Email distribution list | 0 | 0 | |
|
||||
| Reaching every member | floor | Phone and text tree | 0 | 0 | |
|
||||
| How we treat each other | safety | Code of conduct (every Rule) | 92 | 92 | |
|
||||
| Accounts, keys and security | safety | Accounts and keys (every Rule) | 92 | 92 | |
|
||||
| Accounts, keys and security | safety | Password manager | 0 | 0 | |
|
||||
| Accounts, keys and security | safety | Security plan | 0 | 0 | |
|
||||
| Meetings and discussion | fitted | How we run meetings | 92 | 92 | |
|
||||
| Meetings and discussion | fitted | Discourse (forum) | 0 | 0 | |
|
||||
| Meetings and discussion | fitted | In-person meetings | 0 | 0 | |
|
||||
| Meetings and discussion | fitted | Video meetings | 0 | 0 | |
|
||||
| Meetings and discussion | fitted | Deciding in threads | 0 | 0 | |
|
||||
| Meetings and discussion | fitted | Loomio | 0 | 0 | |
|
||||
|
||||
0 of 92 groups have more than seven recommended in this deck. Recommended cards per group run from 6 to 7.
|
||||
|
||||
## What decided each pick
|
||||
|
||||
The 27 Oct ruling on call 1 asks this run to report, job by job, how many picks row order settled. Only means the job had one eligible card. Score means scale, stage or the answer point. Other kinds means the group's other kinds broke a tie. Row order means the winner tied on all of those and came first in the job.
|
||||
|
||||
| Deck | Job | Only | Score | Other kinds | Row order |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Decisions | Everyday decisions | 0 | 0 | 15 | 77 |
|
||||
| Decisions | Big decisions | 0 | 21 | 33 | 38 |
|
||||
| Decisions | Changing this Rule | 2 | 0 | 34 | 56 |
|
||||
| Decisions | Emergencies | 92 | 0 | 0 | 0 |
|
||||
| Decisions | Money | 1 | 1 | 18 | 72 |
|
||||
| Decisions | Spending | 75 | 0 | 0 | 0 |
|
||||
| Decisions | Roles and delegation | 0 | 0 | 29 | 63 |
|
||||
| Membership | Joining | 2 | 0 | 0 | 90 |
|
||||
| Membership | Levels of membership | 18 | 5 | 3 | 66 |
|
||||
| Membership | Learning how we work | 80 | 0 | 0 | 12 |
|
||||
| Membership | Staying and leaving | 48 | 0 | 5 | 39 |
|
||||
| Membership | Removal | 92 | 0 | 0 | 0 |
|
||||
| Conflict | First steps | 92 | 0 | 0 | 0 |
|
||||
| Conflict | Safety first | 49 | 0 | 0 | 43 |
|
||||
| Conflict | Who hears serious cases | 80 | 0 | 0 | 12 |
|
||||
| Conflict | Repair and removal | 2 | 0 | 34 | 56 |
|
||||
| Conflict | Outside help and legal duties | 39 | 34 | 10 | 9 |
|
||||
| Conflict | Legal matters | 37 | 0 | 33 | 22 |
|
||||
| Conflict | Police and arrests | 49 | 0 | 0 | 43 |
|
||||
| Conflict | Prevention | 2 | 0 | 34 | 56 |
|
||||
| Communication | Day-to-day chat | 7 | 0 | 0 | 85 |
|
||||
| Communication | Our records | 91 | 0 | 0 | 1 |
|
||||
| Communication | Records about people | 90 | 0 | 0 | 0 |
|
||||
| Communication | Reaching every member | 2 | 0 | 34 | 56 |
|
||||
| Communication | How we treat each other | 92 | 0 | 0 | 0 |
|
||||
| Communication | Accounts, keys and security | 0 | 0 | 0 | 92 |
|
||||
| Communication | Meetings and discussion | 2 | 0 | 34 | 56 |
|
||||
| All | All jobs | 1044 | 61 | 316 | 1044 |
|
||||
|
||||
Of 2465 picks for the 92 groups, row order settled 1044, other kinds 316 and fit 61, and 1044 had one eligible card.
|
||||
|
||||
## A whole Rule
|
||||
|
||||
A recommended Rule runs 26 to 31 cards, 28.4 on average, and the four sevens show 25 to 28, 26.5 on average.
|
||||
|
||||
The 6 one-member founders get 16 to 22 cards. Their picks are at the end of profiles-picks.md.
|
||||
@@ -30,14 +30,14 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Signal")).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Add Platform")).toBeInTheDocument();
|
||||
expect(within(dialog).getByText("Add card")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("re-opening a selected method shows Save and footer Remove; Remove stays in the kebab", async () => {
|
||||
@@ -45,11 +45,11 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
const dialog = screen.getByRole("dialog");
|
||||
await user.click(within(dialog).getByRole("button", { name: "Add Platform" }));
|
||||
await user.click(within(dialog).getByRole("button", { name: "Add card" }));
|
||||
|
||||
await user.click(signalCards[0]);
|
||||
const dialogAgain = screen.getByRole("dialog");
|
||||
@@ -57,7 +57,7 @@ describe("Create flow communication-methods page", () => {
|
||||
within(dialogAgain).getByRole("button", { name: "Remove" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).queryByRole("button", { name: "Add Platform" }),
|
||||
within(dialogAgain).queryByRole("button", { name: "Add card" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||
@@ -72,12 +72,12 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
await user.click(
|
||||
within(screen.getByRole("dialog")).getByRole("button", {
|
||||
name: "Add Platform",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -96,12 +96,12 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
await user.click(
|
||||
within(screen.getByRole("dialog")).getByRole("button", {
|
||||
name: "Add Platform",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
const dialog = screen.getByRole("dialog");
|
||||
@@ -138,7 +138,7 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const signalCards = screen.getAllByRole("button", {
|
||||
name: /Signal: Encrypted messaging/,
|
||||
name: /Signal: We coordinate in encrypted Signal groups/,
|
||||
});
|
||||
await user.click(signalCards[0]);
|
||||
|
||||
@@ -182,7 +182,7 @@ describe("Create flow communication-methods page", () => {
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
"How should this community communicate with each-other?",
|
||||
"How does your group talk, meet and keep its records?",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -191,11 +191,11 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByText(/You can select multiple methods for different needs or/),
|
||||
screen.getByText(/Pick a card for each part of how your group communicates/),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /^add$/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "See all communication approaches" }),
|
||||
screen.getByRole("button", { name: "See all communication cards" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -230,7 +230,7 @@ describe("Create flow communication-methods page", () => {
|
||||
render(<CommunicationMethodsScreen />);
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all communication approaches",
|
||||
name: "See all communication cards",
|
||||
});
|
||||
await user.click(toggle);
|
||||
|
||||
@@ -239,7 +239,7 @@ describe("Create flow communication-methods page", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
"How should this community communicate with each-other?",
|
||||
"How does your group talk, meet and keep its records?",
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
@@ -419,7 +419,7 @@ describe("Create flow communication-methods page", () => {
|
||||
const dupId = dupIds[0];
|
||||
expect(latest.selectedCommunicationMethodIds).toEqual([CUSTOM_POLICY_ID]);
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Add Platform" }),
|
||||
within(dialog).getByRole("button", { name: "Add card" }),
|
||||
).toBeInTheDocument();
|
||||
expect(metaAfterDup[dupId].label.endsWith(" (copy)")).toBe(true);
|
||||
expect(within(dialog).getByRole("heading", { level: 1 })).toHaveTextContent(
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole("heading", {
|
||||
name: "How should this community make difficult decisions?",
|
||||
name: "How does your group decide, for small things and for big ones?",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -46,8 +46,8 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
const description = addControl.parentElement;
|
||||
expect(description).not.toBeNull();
|
||||
expect(description?.textContent).toMatch(/Select as many as you need/);
|
||||
expect(description?.textContent).toMatch(/new decision making approaches/);
|
||||
expect(description?.textContent).toMatch(/Pick a card for each kind of decision/);
|
||||
expect(description?.textContent).toMatch(/your own/);
|
||||
});
|
||||
|
||||
test("with a finalized custom approach, sidebar still shows add (not Remove policy)", () => {
|
||||
@@ -71,41 +71,41 @@ describe("Create flow decision-approaches page", () => {
|
||||
const addControl = screen.getByRole("button", { name: /^Add$/i });
|
||||
expect(addControl).toBeInTheDocument();
|
||||
const description = addControl.parentElement;
|
||||
expect(description?.textContent).toMatch(/Select as many as you need/);
|
||||
expect(description?.textContent).toMatch(/new decision making approaches/);
|
||||
expect(description?.textContent).toMatch(/Pick a card for each kind of decision/);
|
||||
expect(description?.textContent).toMatch(/your own/);
|
||||
});
|
||||
|
||||
test("renders message box with title and checkboxes", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const region = screen.getByRole("region", {
|
||||
name: "Consider defining approaches to steward key resources:",
|
||||
name: "Make sure your Rule covers each of these",
|
||||
});
|
||||
expect(region).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
within(region).getByRole("checkbox", {
|
||||
name: "Amend your CommunityRule",
|
||||
name: "Everyday decisions",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(region).getByRole("checkbox", { name: "Steward finances" }),
|
||||
within(region).getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(region).getByRole("checkbox", { name: "Project level decisions" }),
|
||||
within(region).getByRole("checkbox", { name: "Spending" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(region).getByRole("checkbox", {
|
||||
name: "Discipline and member termination",
|
||||
name: "Roles and working groups",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders card stack with See all decision approaches toggle", () => {
|
||||
test("renders card stack with See all decision cards toggle", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "See all decision approaches" }),
|
||||
screen.getByRole("button", { name: "See all decision cards" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -114,17 +114,17 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||
name: /Do-ocracy: Whoever does the work/,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", {
|
||||
name: /Consensus Decision-Making: All members must agree/,
|
||||
name: /Decision speeds: We sort each decision/,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -134,7 +134,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all decision approaches",
|
||||
name: "See all decision cards",
|
||||
});
|
||||
await user.click(toggle);
|
||||
|
||||
@@ -149,16 +149,16 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /^Sociocracy:/ }),
|
||||
screen.queryByRole("button", { name: /^Consent \(sociocracy\):/ }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const toggle = screen.getByRole("button", {
|
||||
name: "See all decision approaches",
|
||||
name: "See all decision cards",
|
||||
});
|
||||
await user.click(toggle);
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: /^Sociocracy:/ }),
|
||||
screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", { name: "See all decision approaches" }),
|
||||
screen.getByRole("button", { name: "See all decision cards" }),
|
||||
);
|
||||
|
||||
const cardLabels = () =>
|
||||
@@ -177,15 +177,15 @@ describe("Create flow decision-approaches page", () => {
|
||||
.filter((label) => label.includes(": "));
|
||||
|
||||
const labelsBefore = cardLabels();
|
||||
expect(labelsBefore[0]).toMatch(/^Lazy Consensus:/);
|
||||
expect(labelsBefore.some((label) => label.startsWith("Sociocracy:"))).toBe(
|
||||
expect(labelsBefore[0]).toMatch(/^Decision speeds:/);
|
||||
expect(labelsBefore.some((label) => label.startsWith("Consent (sociocracy):"))).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /^Sociocracy:/ }));
|
||||
await user.click(screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }));
|
||||
await user.click(
|
||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||
name: "Add Approach",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
await waitFor(() => {
|
||||
@@ -197,7 +197,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(cardLabels()).toEqual(labelsBefore);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /^Sociocracy:/ }),
|
||||
screen.getByRole("button", { name: /^Consent \(sociocracy\):/ }),
|
||||
).toHaveTextContent("SELECTED");
|
||||
});
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
@@ -229,7 +229,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
|
||||
@@ -237,7 +237,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
expect(dialog).toBeInTheDocument();
|
||||
|
||||
const confirmButton = within(dialog).getByRole("button", {
|
||||
name: "Add Approach",
|
||||
name: "Add card",
|
||||
});
|
||||
await user.click(confirmButton);
|
||||
|
||||
@@ -249,13 +249,13 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Add Approach",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -265,7 +265,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
within(dialogAgain).getByRole("button", { name: "Remove" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).queryByRole("button", { name: "Add Approach" }),
|
||||
within(dialogAgain).queryByRole("button", { name: "Add card" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
within(dialogAgain).getByRole("button", { name: "Save" }),
|
||||
@@ -280,12 +280,12 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
await user.click(
|
||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||
name: "Add Approach",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -304,12 +304,12 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(dialog).getByRole("button", { name: "Add Approach" }),
|
||||
within(dialog).getByRole("button", { name: "Add card" }),
|
||||
);
|
||||
|
||||
await user.click(card);
|
||||
@@ -331,12 +331,12 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
await user.click(
|
||||
within(await screen.findByRole("dialog")).getByRole("button", {
|
||||
name: "Add Approach",
|
||||
name: "Add card",
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -363,7 +363,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
);
|
||||
|
||||
const card = screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
});
|
||||
await user.click(card);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
@@ -380,7 +380,7 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
const amendCheckbox = screen.getByRole("checkbox", {
|
||||
name: "Amend your CommunityRule",
|
||||
name: "Changing this Rule",
|
||||
});
|
||||
expect(amendCheckbox).not.toBeChecked();
|
||||
await user.click(amendCheckbox);
|
||||
@@ -393,26 +393,26 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const dialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Select Amend your CommunityRule",
|
||||
name: "Select Changing this Rule",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", { name: "Select Steward finances" }),
|
||||
within(dialog).getByRole("button", { name: "Select Shared money" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Select Project level decisions",
|
||||
name: "Select Big decisions",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(dialog).getByRole("button", {
|
||||
name: "Select Discipline and member termination",
|
||||
name: "Select Spending",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -422,18 +422,18 @@ describe("Create flow decision-approaches page", () => {
|
||||
render(<DecisionApproachesScreen />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyDialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(lazyDialog).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -444,20 +444,20 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyDialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(lazyDialog).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeChecked();
|
||||
await user.click(
|
||||
within(lazyDialog).getByRole("button", { name: "Add Approach" }),
|
||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
@@ -465,22 +465,22 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||
name: /Do-ocracy: Whoever does the work/,
|
||||
}),
|
||||
);
|
||||
const doocracyDialog = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(doocracyDialog).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(doocracyDialog).queryByRole("button", {
|
||||
name: "Deselect Steward finances",
|
||||
name: "Deselect Shared money",
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).not.toBeChecked();
|
||||
await user.click(
|
||||
within(doocracyDialog).getByRole("button", { name: "Close" }),
|
||||
@@ -490,22 +490,22 @@ describe("Create flow decision-approaches page", () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeChecked();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyAgain = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(lazyAgain).getByRole("button", {
|
||||
name: "Deselect Steward finances",
|
||||
name: "Deselect Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeChecked();
|
||||
});
|
||||
|
||||
@@ -515,12 +515,12 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyDialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(lazyDialog).getByRole("button", { name: "Add Approach" }),
|
||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
@@ -528,16 +528,16 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||
name: /Do-ocracy: Whoever does the work/,
|
||||
}),
|
||||
);
|
||||
const doocracyDialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
);
|
||||
expect(
|
||||
within(doocracyDialog).getByRole("button", {
|
||||
name: "Deselect Steward finances",
|
||||
name: "Deselect Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
@@ -550,13 +550,13 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyAgain = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(lazyAgain).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
@@ -567,20 +567,20 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyDialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(lazyDialog).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeChecked();
|
||||
await user.click(
|
||||
within(lazyDialog).getByRole("button", { name: "Add Approach" }),
|
||||
within(lazyDialog).getByRole("button", { name: "Add card" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
@@ -588,54 +588,54 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||
name: /Do-ocracy: Whoever does the work/,
|
||||
}),
|
||||
);
|
||||
const doocracyDialog = await screen.findByRole("dialog");
|
||||
await user.click(
|
||||
within(doocracyDialog).getByRole("button", {
|
||||
name: "Select Project level decisions",
|
||||
name: "Select Big decisions",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Project level decisions" }),
|
||||
screen.getByRole("checkbox", { name: "Big decisions" }),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
within(doocracyDialog).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
within(doocracyDialog).getByRole("button", { name: "Add Approach" }),
|
||||
within(doocracyDialog).getByRole("button", { name: "Add card" }),
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Steward finances" }),
|
||||
screen.getByRole("checkbox", { name: "Shared money" }),
|
||||
).toBeChecked();
|
||||
expect(
|
||||
screen.getByRole("checkbox", { name: "Project level decisions" }),
|
||||
screen.getByRole("checkbox", { name: "Big decisions" }),
|
||||
).toBeChecked();
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Lazy Consensus: A decision is assumed approved/,
|
||||
name: /Lazy consensus: A proposal passes/,
|
||||
}),
|
||||
);
|
||||
const lazyAgain = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(lazyAgain).getByRole("button", {
|
||||
name: "Deselect Steward finances",
|
||||
name: "Deselect Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(lazyAgain).getByRole("button", {
|
||||
name: "Select Project level decisions",
|
||||
name: "Select Big decisions",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
await user.click(
|
||||
@@ -647,18 +647,18 @@ describe("Create flow decision-approaches page", () => {
|
||||
|
||||
await user.click(
|
||||
screen.getByRole("button", {
|
||||
name: /Do-ocracy: Decisions are made by those who take initiative/,
|
||||
name: /Do-ocracy: Whoever does the work/,
|
||||
}),
|
||||
);
|
||||
const doocracyAgain = await screen.findByRole("dialog");
|
||||
expect(
|
||||
within(doocracyAgain).getByRole("button", {
|
||||
name: "Deselect Project level decisions",
|
||||
name: "Deselect Big decisions",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
within(doocracyAgain).getByRole("button", {
|
||||
name: "Select Steward finances",
|
||||
name: "Select Shared money",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("Page Flow Integration", () => {
|
||||
screen.getByRole("heading", { name: "Do-ocracy" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("heading", { name: "Quadratic Governance" }),
|
||||
screen.getByRole("heading", { name: "Solidarity Network" }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -155,10 +155,10 @@ describe("Page Flow Integration", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Devolution")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText("Solidarity Network")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Quadratic Governance")).not.toBeInTheDocument();
|
||||
expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument();
|
||||
expect(screen.getByText("Solidarity Network")).toBeInTheDocument();
|
||||
|
||||
const seeAll = screen.getByRole("link", { name: "See all templates" });
|
||||
expect(seeAll).toHaveAttribute("href", "/templates");
|
||||
|
||||
@@ -60,7 +60,7 @@ describe("User Journey Integration", () => {
|
||||
});
|
||||
expect(screen.getAllByText("Consensus").length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText("Do-ocracy").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Quadratic Governance")).toBeInTheDocument();
|
||||
expect(screen.getByText("Solidarity Network")).toBeInTheDocument();
|
||||
|
||||
const seeHowLinks = screen.getAllByRole("link", {
|
||||
name: "See how it works",
|
||||
|
||||
@@ -105,9 +105,7 @@ describe("RuleStack Component", () => {
|
||||
expect(GOVERNANCE_TEMPLATE_CATALOG.length).toBeGreaterThan(
|
||||
homeFeatured.length,
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("Solidarity Network"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Quadratic Governance")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with custom className", async () => {
|
||||
@@ -139,7 +137,7 @@ describe("RuleStack Component", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Voting cost is squared \(V²\), preventing a majority from steamrolling a passionate minority\./,
|
||||
/Power is held by autonomous "cells\." A central hub acts as a switchboard for resources but cannot dictate cell activities\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -7,14 +7,12 @@ import { methodSectionsPinsForHydratedSelections } from "../../lib/create/publis
|
||||
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
|
||||
|
||||
function coreValuePresetId(label: string): string {
|
||||
const values = coreValuesMessages.values as Array<
|
||||
string | { label: string }
|
||||
>;
|
||||
const idx = values.findIndex((v) => {
|
||||
const l = typeof v === "string" ? v : v.label;
|
||||
return l.toLowerCase() === label.toLowerCase();
|
||||
});
|
||||
return String(idx + 1);
|
||||
const values = coreValuesMessages.values as Array<{ id: string; label: string }>;
|
||||
const row = values.find(
|
||||
(value) => value.label.toLowerCase() === label.toLowerCase(),
|
||||
);
|
||||
if (!row) throw new Error(`No core value named ${label}`);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe("buildTemplateCustomizePrefill", () => {
|
||||
@@ -76,7 +74,7 @@ describe("buildTemplateCustomizePrefill", () => {
|
||||
const snapshot = prefill.coreValuesChipsSnapshot ?? [];
|
||||
const selectedRows = snapshot.filter((r) => r.state === "selected");
|
||||
expect(selectedRows.map((r) => r.label).sort()).toEqual([
|
||||
"Community Care",
|
||||
"Community care",
|
||||
"Consensus",
|
||||
]);
|
||||
// Unmatched presets should still appear, as unselected, so the screen
|
||||
|
||||
@@ -20,19 +20,19 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
// Same shape `buildTemplateCustomizePrefill` emits via methodSlugFromTitle.
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "in-person-meetings"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
const byName = new Map(rows.map((r) => [r.name, r.chips]));
|
||||
expect(byName.get("Communication")).toEqual([
|
||||
"Signal",
|
||||
"In-Person Meetings",
|
||||
"In-person meetings",
|
||||
]);
|
||||
expect(byName.get("Membership")).toEqual(["Open Access"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy Consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Peer Mediation"]);
|
||||
expect(byName.get("Membership")).toEqual(["Open admission"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Conflict ladder"]);
|
||||
expect(byName.has("Values")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Communication", chips: ["In-Person Meetings"] },
|
||||
{ name: "Communication", chips: ["In-person meetings"] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("buildPublishPayload", () => {
|
||||
},
|
||||
],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +62,7 @@ describe("buildPublishPayload", () => {
|
||||
},
|
||||
],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +87,7 @@ describe("buildPublishPayload", () => {
|
||||
const r = buildPublishPayload({ title: "T", sections });
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.document).toEqual({ sections, coreValues: [] });
|
||||
expect(r.document).toEqual({ sections, coreValues: [], supermajorityShare: 75 });
|
||||
});
|
||||
|
||||
it("filters invalid section entries from state.sections", () => {
|
||||
@@ -101,6 +103,7 @@ describe("buildPublishPayload", () => {
|
||||
expect(r.document).toEqual({
|
||||
sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,9 +334,9 @@ describe("buildPublishPayload — methodSelections", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { CommunityStructureChipSnapshotRow } from "../../app/(app)/create/t
|
||||
import { buildCoreValueChipOptionsFromDraft } from "../../lib/create/coreValueChipOptionsFromDraft";
|
||||
|
||||
const PRESETS = [
|
||||
{ label: "A", meaning: "am", signals: "as" },
|
||||
{ label: "B", meaning: "bm", signals: "bs" },
|
||||
{ label: "C", meaning: "cm", signals: "cs" },
|
||||
{ id: "1", label: "A", meaning: "am", signals: "as" },
|
||||
{ id: "2", label: "B", meaning: "bm", signals: "bs" },
|
||||
{ id: "3", label: "C", meaning: "cm", signals: "cs" },
|
||||
] as const;
|
||||
|
||||
describe("buildCoreValueChipOptionsFromDraft", () => {
|
||||
|
||||
@@ -100,27 +100,26 @@ describe("GET /api/create-flow/methods", () => {
|
||||
};
|
||||
expect(json.section).toBe("communication");
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].slug).toBe("loomio");
|
||||
expect(json.methods[0].matches.score).toBe(2);
|
||||
expect(json.methods[1].slug).toBe("in-person-meetings");
|
||||
expect(json.methods[1].matches.score).toBe(1);
|
||||
expect(json.methods[0].matches.score).toBeGreaterThan(json.methods[1].matches.score);
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns full catalog without matches when the DB query throws", async () => {
|
||||
it("ranks from the deck files when the database is down", async () => {
|
||||
findManyMock.mockRejectedValueOnce(new Error("db down"));
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
"https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember",
|
||||
"https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember&facet.orgType=workersCoop",
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
methods: Array<{ slug: string; matches?: unknown }>;
|
||||
methods: Array<{ slug: string; matches?: { score: number } }>;
|
||||
};
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].matches).toBeUndefined();
|
||||
expect(json.methods[0].matches?.score).toBeGreaterThan(0);
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns all core values for section=coreValues", async () => {
|
||||
@@ -137,7 +136,7 @@ describe("GET /api/create-flow/methods", () => {
|
||||
};
|
||||
expect(json.section).toBe("coreValues");
|
||||
expect(json.methods.length).toBeGreaterThan(50);
|
||||
expect(json.methods[0].id).toBe("1");
|
||||
expect(json.methods[0].id).toBe("accessibility");
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -23,39 +23,42 @@ const emptyEntry = (): DecisionApproachDetailEntry => ({
|
||||
});
|
||||
|
||||
describe("decisionApproachKeyResources", () => {
|
||||
it("reads the four key-resource items from messages", () => {
|
||||
it("reads the decision checklist from messages", () => {
|
||||
expect(decisionApproachKeyResourceItems()).toEqual([
|
||||
{ id: "amend", label: "Amend your CommunityRule" },
|
||||
{ id: "finances", label: "Steward finances" },
|
||||
{ id: "project", label: "Project level decisions" },
|
||||
{ id: "discipline", label: "Discipline and member termination" },
|
||||
{ id: "everyday", label: "Everyday decisions" },
|
||||
{ id: "big", label: "Big decisions" },
|
||||
{ id: "amend", label: "Changing this Rule" },
|
||||
{ id: "emergency", label: "Emergencies" },
|
||||
{ id: "finances", label: "Shared money" },
|
||||
{ id: "spending", label: "Spending" },
|
||||
{ id: "roles", label: "Roles and working groups" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects checked key-resource labels without rewriting applicableScope", () => {
|
||||
const next = applyDecisionApproachKeyResources(emptyEntry(), [
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
expect(next.applicableScope).toEqual(["Daily Operations"]);
|
||||
expect(next.selectedApplicableScope).toEqual(["Steward finances"]);
|
||||
expect(next.selectedApplicableScope).toEqual(["Shared money"]);
|
||||
});
|
||||
|
||||
it("keeps non-key chip selections when syncing sidebar checks", () => {
|
||||
const next = applyDecisionApproachKeyResources(
|
||||
{
|
||||
...emptyEntry(),
|
||||
applicableScope: ["Steward finances", "Daily Operations"],
|
||||
applicableScope: ["Shared money", "Daily Operations"],
|
||||
selectedApplicableScope: ["Daily Operations"],
|
||||
},
|
||||
["Steward finances"],
|
||||
["Shared money"],
|
||||
);
|
||||
expect(next.applicableScope).toEqual([
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
"Daily Operations",
|
||||
]);
|
||||
expect(next.selectedApplicableScope).toEqual([
|
||||
"Daily Operations",
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -63,19 +66,19 @@ describe("decisionApproachKeyResources", () => {
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
["Steward finances"],
|
||||
["Shared money"],
|
||||
),
|
||||
).toEqual([
|
||||
"Daily Operations",
|
||||
"Minor Expenditures",
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
["Daily Operations", "Steward finances"],
|
||||
["Daily Operations", "Shared money"],
|
||||
),
|
||||
).toEqual(["Daily Operations", "Steward finances"]);
|
||||
).toEqual(["Daily Operations", "Shared money"]);
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
@@ -85,44 +88,44 @@ describe("decisionApproachKeyResources", () => {
|
||||
});
|
||||
|
||||
it("maps checkbox ids to labels and back", () => {
|
||||
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "project"])).toEqual(
|
||||
["Amend your CommunityRule", "Project level decisions"],
|
||||
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "big"])).toEqual(
|
||||
["Big decisions", "Changing this Rule"],
|
||||
);
|
||||
expect(
|
||||
decisionApproachKeyResourceIdsFromLabels([
|
||||
"Project level decisions",
|
||||
"Big decisions",
|
||||
"Unknown",
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]),
|
||||
).toEqual(["amend", "project"]);
|
||||
).toEqual(["big", "amend"]);
|
||||
});
|
||||
|
||||
it("derives sidebar checks from the open draft or selected approaches", () => {
|
||||
const detailsById = {
|
||||
"lazy-consensus": {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Steward finances"],
|
||||
selectedApplicableScope: ["Shared money"],
|
||||
},
|
||||
"do-ocracy": {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Project level decisions"],
|
||||
selectedApplicableScope: ["Big decisions"],
|
||||
},
|
||||
};
|
||||
expect(
|
||||
decisionApproachKeyResourceCheckboxIds({
|
||||
detailsById,
|
||||
selectedApproachIds: ["lazy-consensus", "do-ocracy"],
|
||||
reminderIds: ["discipline"],
|
||||
reminderIds: ["spending"],
|
||||
}),
|
||||
).toEqual(["finances", "project", "discipline"]);
|
||||
).toEqual(["big", "finances", "spending"]);
|
||||
expect(
|
||||
decisionApproachKeyResourceCheckboxIds({
|
||||
detailsById,
|
||||
selectedApproachIds: ["lazy-consensus", "do-ocracy"],
|
||||
reminderIds: ["discipline"],
|
||||
reminderIds: ["spending"],
|
||||
openDraft: {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Amend your CommunityRule"],
|
||||
selectedApplicableScope: ["Changing this Rule"],
|
||||
},
|
||||
}),
|
||||
).toEqual(["amend"]);
|
||||
@@ -132,7 +135,7 @@ describe("decisionApproachKeyResources", () => {
|
||||
const next = syncDecisionApproachKeyResourceDetails(
|
||||
{ "lazy-consensus": emptyEntry() },
|
||||
["do-ocracy"],
|
||||
["Amend your CommunityRule"],
|
||||
["Changing this Rule"],
|
||||
(id) => ({
|
||||
...emptyEntry(),
|
||||
corePrinciple: id,
|
||||
@@ -140,19 +143,19 @@ describe("decisionApproachKeyResources", () => {
|
||||
}),
|
||||
);
|
||||
expect(next["lazy-consensus"]?.selectedApplicableScope).toEqual([
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]);
|
||||
expect(next["do-ocracy"]?.corePrinciple).toBe("do-ocracy");
|
||||
expect(next["do-ocracy"]?.applicableScope).toEqual(["Volunteer Tasks"]);
|
||||
expect(next["do-ocracy"]?.selectedApplicableScope).toEqual([
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compares string arrays by order", () => {
|
||||
expect(stringArraysEqual(["a", "b"], ["a", "b"])).toBe(true);
|
||||
expect(stringArraysEqual(["a", "b"], ["b", "a"])).toBe(false);
|
||||
expect(decisionApproachKeyResourceLabels()).toHaveLength(4);
|
||||
expect(decisionApproachKeyResourceLabels()).toHaveLength(7);
|
||||
});
|
||||
|
||||
it("unions key-resource labels onto an existing scope list", () => {
|
||||
@@ -160,10 +163,13 @@ describe("decisionApproachKeyResources", () => {
|
||||
withDecisionApproachKeyResourceScopes(["Daily Operations"]),
|
||||
).toEqual([
|
||||
"Daily Operations",
|
||||
"Amend your CommunityRule",
|
||||
"Steward finances",
|
||||
"Project level decisions",
|
||||
"Discipline and member termination",
|
||||
"Everyday decisions",
|
||||
"Big decisions",
|
||||
"Changing this Rule",
|
||||
"Emergencies",
|
||||
"Shared money",
|
||||
"Spending",
|
||||
"Roles and working groups",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,93 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { deriveCompactCards } from "../../app/(app)/create/hooks/useFacetRecommendations";
|
||||
|
||||
const methods = [
|
||||
{ id: "alpha" },
|
||||
{ id: "bravo" },
|
||||
{ id: "charlie" },
|
||||
{ id: "delta" },
|
||||
{ id: "echo" },
|
||||
{ id: "foxtrot" },
|
||||
{ id: "golf" },
|
||||
] as const;
|
||||
|
||||
describe("deriveCompactCards", () => {
|
||||
it("falls back to authoring order with no badges when no facets selected", () => {
|
||||
const result = deriveCompactCards(methods, {}, false, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
it("falls back to deck order with no tags when no facets are chosen", () => {
|
||||
const result = deriveCompactCards("decisionApproaches", {}, 5);
|
||||
expect(result.compactCardIds).toHaveLength(5);
|
||||
expect(result.recommendedIds.size).toBe(0);
|
||||
expect(result.orderedIds[0]).toBe(result.compactCardIds[0]);
|
||||
});
|
||||
|
||||
it("falls back to authoring order with no badges when facets selected but every score is zero", () => {
|
||||
const result = deriveCompactCards(methods, {}, true, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
expect(result.recommendedIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("shows only recommended (matched) cards when fewer than the limit match", () => {
|
||||
it("shows the top five in deck order and leaves every-rule cards untagged", () => {
|
||||
const result = deriveCompactCards(
|
||||
methods,
|
||||
{ bravo: 2, delta: 1 },
|
||||
true,
|
||||
"decisionApproaches",
|
||||
{
|
||||
size: "thirteenToOneHundred",
|
||||
mainKind: "workersCoop",
|
||||
scale: ["local"],
|
||||
stage: "established",
|
||||
traits: { risky: true, sponsor: true, employer: true },
|
||||
},
|
||||
5,
|
||||
);
|
||||
// Caller is responsible for pre-ranking by score (rankMethodsByScore).
|
||||
// This test passes already-ranked input; the hook just respects ordering
|
||||
// and tags only the matched subset — no padding with unrecommended cards.
|
||||
expect(result.compactCardIds).toEqual(["bravo", "delta"]);
|
||||
expect([...result.recommendedIds].sort()).toEqual(["bravo", "delta"]);
|
||||
});
|
||||
|
||||
it("caps recommended cards at the limit when more than `limit` match", () => {
|
||||
const scores = {
|
||||
alpha: 4,
|
||||
bravo: 3,
|
||||
charlie: 3,
|
||||
delta: 2,
|
||||
echo: 1,
|
||||
foxtrot: 1,
|
||||
golf: 1,
|
||||
};
|
||||
const result = deriveCompactCards(methods, scores, true, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
expect(result.recommendedIds.size).toBe(5);
|
||||
expect([...result.recommendedIds].sort()).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
"decision-speeds",
|
||||
"modified-consensus",
|
||||
"emergency-decisions",
|
||||
"on-site-lead",
|
||||
"how-we-are-covered",
|
||||
]);
|
||||
expect(result.recommendedIds.has("decision-speeds")).toBe(false);
|
||||
expect(result.recommendedIds.has("on-site-lead")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns a single card when only one method matches", () => {
|
||||
const result = deriveCompactCards(methods, { charlie: 4 }, true, 5);
|
||||
expect(result.compactCardIds).toEqual(["charlie"]);
|
||||
expect([...result.recommendedIds]).toEqual(["charlie"]);
|
||||
});
|
||||
|
||||
it("respects a smaller `limit` even when many methods match", () => {
|
||||
const scores = { alpha: 4, bravo: 3, charlie: 3, delta: 2 };
|
||||
const result = deriveCompactCards(methods, scores, true, 3);
|
||||
expect(result.compactCardIds).toEqual(["alpha", "bravo", "charlie"]);
|
||||
expect(result.recommendedIds.size).toBe(3);
|
||||
it("shows seven when the screen asks for seven, with spending behind see all", () => {
|
||||
const result = deriveCompactCards(
|
||||
"decisionApproaches",
|
||||
{
|
||||
size: "thirteenToOneHundred",
|
||||
mainKind: "workersCoop",
|
||||
scale: ["local"],
|
||||
stage: "established",
|
||||
traits: { risky: true, sponsor: true, employer: true },
|
||||
},
|
||||
7,
|
||||
);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"decision-speeds",
|
||||
"modified-consensus",
|
||||
"changing-this-rule",
|
||||
"emergency-decisions",
|
||||
"on-site-lead",
|
||||
"how-we-are-covered",
|
||||
"elected-board-of-directors",
|
||||
]);
|
||||
expect(result.orderedIds.slice(7, 10)).toEqual([
|
||||
"shared-money",
|
||||
"spending-limits",
|
||||
"pay-and-hiring",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -46,16 +46,16 @@ describe("governanceCatalog (CR-115)", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("core values: ids are 1-based positions with copy fields", () => {
|
||||
it("core values: ids come from the value id field", () => {
|
||||
const values = listCatalogCoreValues();
|
||||
const messages = readJson<{
|
||||
values: Array<string | { label: string }>;
|
||||
values: Array<{ id: string; label: string }>;
|
||||
}>("messages/en/create/customRule/coreValues.json");
|
||||
expect(values.length).toBe(messages.values.length);
|
||||
expect(values[0].id).toBe("1");
|
||||
expect(values[0].label).toBeTruthy();
|
||||
expect(values[0].id).toBe(messages.values[0].id);
|
||||
expect(values[0].label).toBe(messages.values[0].label);
|
||||
expect(typeof values[0].meaning).toBe("string");
|
||||
expect(typeof values[0].signals).toBe("string");
|
||||
expect(getCatalogCoreValue("1")).toEqual(values[0]);
|
||||
expect(getCatalogCoreValue(values[0].id)).toEqual(values[0]);
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe("governanceTemplateCatalog (Figma 21764-16435)", () => {
|
||||
"consensus",
|
||||
"do-ocracy",
|
||||
"devolution",
|
||||
"quadratic-governance",
|
||||
"solidarity-network",
|
||||
]);
|
||||
expect(getGovernanceTemplatesForHome()).toHaveLength(4);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
|
||||
import { groupAnswersFromCreateFlowState } from "../../lib/create/groupAnswers";
|
||||
|
||||
describe("maturity and chip aliases", () => {
|
||||
it("scores one maturity answer and keeps a draft that stored several", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["1", "2", "4"],
|
||||
selectedOrganizationTypeIds: ["1"],
|
||||
});
|
||||
expect(draft.selectedMaturityIds).toEqual([
|
||||
"earlyStage",
|
||||
"growthStage",
|
||||
"established",
|
||||
]);
|
||||
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
|
||||
const answers = groupAnswersFromCreateFlowState(draft);
|
||||
expect(answers.stage).toBe("earlyStage");
|
||||
expect(answers.mainKind).toBe("workersCoop");
|
||||
});
|
||||
|
||||
it("loads Enterprise and Larger organization as Established", () => {
|
||||
const fromId = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["4"],
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: [
|
||||
{ id: "1", label: "Early stage", state: "unselected" },
|
||||
{ id: "2", label: "Growth stage", state: "unselected" },
|
||||
{ id: "3", label: "Established", state: "unselected" },
|
||||
{ id: "4", label: "Enterprise", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(fromId.selectedMaturityIds).toEqual(["established"]);
|
||||
expect(
|
||||
fromId.communityStructureChipSnapshots?.maturity?.find(
|
||||
(row) => row.id === "established",
|
||||
)?.label,
|
||||
).toBe("Established");
|
||||
|
||||
const fromLabel = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["larger-organization"],
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: [
|
||||
{ id: "larger-organization", label: "Larger organization", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(fromLabel.selectedMaturityIds).toEqual(["established"]);
|
||||
});
|
||||
|
||||
it("keeps the worker co-op pick when the label was Worker cooperative", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedOrganizationTypeIds: ["1"],
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: [
|
||||
{ id: "1", label: "Worker cooperative", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
|
||||
expect(
|
||||
draft.communityStructureChipSnapshots?.organizationTypes?.find(
|
||||
(row) => row.id === "workersCoop",
|
||||
)?.label,
|
||||
).toBe("Worker co-op");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
|
||||
import { resolveCoreValueId, resolveMethodId } from "../../lib/create/idAliases";
|
||||
|
||||
describe("deck id aliases", () => {
|
||||
it("maps old card ids onto the remade decks", () => {
|
||||
expect(resolveMethodId("membership", "open-access")).toEqual({
|
||||
deck: "membership",
|
||||
id: "open-admission",
|
||||
});
|
||||
expect(resolveMethodId("conflictManagement", "peer-mediation")).toEqual({
|
||||
deck: "conflictManagement",
|
||||
id: "conflict-ladder",
|
||||
});
|
||||
expect(resolveMethodId("decisionApproaches", "lazy-consensus")).toEqual({
|
||||
deck: "decisionApproaches",
|
||||
id: "lazy-consensus",
|
||||
});
|
||||
});
|
||||
|
||||
it("moves a cut card to the card that now holds its content", () => {
|
||||
expect(resolveMethodId("decisionApproaches", "proof-of-work")).toEqual({
|
||||
deck: "decisionApproaches",
|
||||
id: "weighted-voting",
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates a saved draft's cards and core values", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedCoreValueIds: ["1", "5", "36"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
{ id: "5", label: "Altruism", state: "selected" },
|
||||
{ id: "36", label: "Holocracy", state: "selected" },
|
||||
],
|
||||
});
|
||||
expect(draft.selectedMembershipMethodIds).toEqual(["open-admission"]);
|
||||
expect(draft.selectedConflictManagementIds).toEqual(["conflict-ladder"]);
|
||||
expect(draft.selectedCoreValueIds).toEqual(["accessibility", "generosity"]);
|
||||
expect(resolveCoreValueId("36")).toBeNull();
|
||||
expect(draft.coreValuesChipsSnapshot?.map((row) => row.id)).toEqual([
|
||||
"accessibility",
|
||||
"generosity",
|
||||
]);
|
||||
expect(draft.coreValuesChipsSnapshot?.[1]?.label).toBe("Generosity");
|
||||
});
|
||||
|
||||
it("is safe to run twice", () => {
|
||||
const once = migrateLegacyCreateFlowState({
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedCoreValueIds: ["2"],
|
||||
});
|
||||
const twice = migrateLegacyCreateFlowState(once);
|
||||
expect(twice.selectedMembershipMethodIds).toEqual(once.selectedMembershipMethodIds);
|
||||
expect(twice.selectedCoreValueIds).toEqual(once.selectedCoreValueIds);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("methodCardFacetMatchesPresetForId", () => {
|
||||
decisionApproachFacetMatchesPreset(
|
||||
{
|
||||
...p,
|
||||
selectedApplicableScope: ["Steward finances"],
|
||||
selectedApplicableScope: ["Shared money"],
|
||||
},
|
||||
uuid,
|
||||
),
|
||||
|
||||
@@ -66,13 +66,20 @@ describe("data/create/customRule/_facetGroups.json (CR-88)", () => {
|
||||
const [filePath, jsonPointer] = block.source.split("#");
|
||||
const file = readJson<Record<string, { label: string }[]>>(filePath);
|
||||
const arrayKey = jsonPointer.replace(/^\//, "");
|
||||
const arr = file[arrayKey];
|
||||
const arr = file[arrayKey] as { id?: string }[];
|
||||
expect(Array.isArray(arr), `${group}: ${block.source} → array`).toBe(true);
|
||||
const positions = new Set(arr.map((_, i) => String(i + 1)));
|
||||
const chipIds = new Set(
|
||||
arr.map((row, i) =>
|
||||
row && typeof row === "object" && "id" in row && typeof row.id === "string"
|
||||
? row.id
|
||||
: String(i + 1),
|
||||
),
|
||||
);
|
||||
for (const [valueId, { chipId }] of Object.entries(block.values)) {
|
||||
const retiredEnterprise = group === "maturity" && valueId === "enterprise";
|
||||
expect(
|
||||
positions.has(chipId),
|
||||
`${group}.${valueId} chipId ${chipId} should be a position in ${block.source} (have ${[...positions].join(",")})`,
|
||||
chipIds.has(chipId) || retiredEnterprise,
|
||||
`${group}.${valueId} chipId ${chipId} should be an option id in ${block.source}`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ beforeEach(() => {
|
||||
findManyMock.mockReset();
|
||||
});
|
||||
|
||||
describe("listMethodRecommendations (CR-88 §9.2)", () => {
|
||||
describe("listMethodRecommendations", () => {
|
||||
it("returns empty rankings when no facets are requested", async () => {
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
@@ -33,55 +33,40 @@ describe("listMethodRecommendations (CR-88 §9.2)", () => {
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scores methods by counting matched (group, value) pairs", async () => {
|
||||
findManyMock.mockResolvedValueOnce([
|
||||
{ slug: "loomio", group: "size", value: "thirteenToOneHundred" },
|
||||
{ slug: "loomio", group: "orgType", value: "workersCoop" },
|
||||
{ slug: "in-person", group: "size", value: "thirteenToOneHundred" },
|
||||
]);
|
||||
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
it("ranks from the job rule and scores maturity once", async () => {
|
||||
const once = await listMethodRecommendations({
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop"],
|
||||
maturity: ["established"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
rankedSlugs: ["loomio", "in-person"],
|
||||
matchesBySlug: {
|
||||
loomio: {
|
||||
score: 2,
|
||||
matchedFacets: ["size:thirteenToOneHundred", "orgType:workersCoop"],
|
||||
},
|
||||
"in-person": {
|
||||
score: 1,
|
||||
matchedFacets: ["size:thirteenToOneHundred"],
|
||||
},
|
||||
const several = await listMethodRecommendations({
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop"],
|
||||
maturity: ["established", "earlyStage", "enterprise"],
|
||||
},
|
||||
});
|
||||
expect(once?.rankedSlugs).toEqual(several?.rankedSlugs);
|
||||
expect(once?.rankedSlugs[0]).toBeTruthy();
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null on query failure (caller falls back to authoring order)", async () => {
|
||||
findManyMock.mockRejectedValueOnce(new Error("db down"));
|
||||
it("uses only the main kind for eligibility", async () => {
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
facets: { size: ["oneMember"] },
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop", "forProfit"],
|
||||
scale: ["local"],
|
||||
maturity: ["established"],
|
||||
},
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("dedupes (group, value) so the same row never double-counts", async () => {
|
||||
findManyMock.mockResolvedValueOnce([
|
||||
{ slug: "loomio", group: "size", value: "twoToFive" },
|
||||
{ slug: "loomio", group: "size", value: "twoToFive" },
|
||||
]);
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
facets: { size: ["twoToFive"] },
|
||||
});
|
||||
expect(result?.matchesBySlug["loomio"]?.score).toBe(1);
|
||||
expect(result?.rankedSlugs).toContain("consent-sociocracy");
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("createFlowStateFromPublishedRule", () => {
|
||||
expect(partial.editingPublishedRuleId).toBe("rule-1");
|
||||
expect(partial.title).toBe("Oak");
|
||||
expect(partial.communityContext).toBe("River cleanup");
|
||||
expect(partial.selectedCoreValueIds).toEqual(["1"]);
|
||||
expect(partial.selectedCoreValueIds).toEqual(["accessibility"]);
|
||||
expect(partial.coreValuesChipsSnapshot?.[0]?.label).toBe("Ecology");
|
||||
expect(partial.selectedCommunicationMethodIds).toEqual(["slack"]);
|
||||
expect(partial.communicationMethodDetailsById?.slack?.corePrinciple).toBe(
|
||||
|
||||
@@ -32,7 +32,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
},
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Communication",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].title).toBe("Ecology");
|
||||
expect(out[0].entries[0].body).toBe(
|
||||
"We protect water.\n\nLitter = violation.",
|
||||
@@ -50,7 +54,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Membership"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Membership",
|
||||
"Shared terms",
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers document.coreValues over a parallel Values section in document.sections", () => {
|
||||
@@ -66,8 +73,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.length).toBe(1);
|
||||
expect(out[0].categoryName).toBe("Values");
|
||||
expect(out.map((section) => section.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].title).toBe("Should not duplicate");
|
||||
expect(out[0].entries[0].body).toBe("x\n\ny");
|
||||
});
|
||||
@@ -80,7 +89,7 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out[0].entries[0].body).toMatch(/survival and success/);
|
||||
expect(out[0].entries[0].body).toMatch(/well-being is bound up/);
|
||||
});
|
||||
|
||||
it("replaces template placeholder bodies with preset copy, Values first", () => {
|
||||
@@ -99,7 +108,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Communication",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].body).not.toContain("Suggested focus");
|
||||
expect(out[0].entries[0].body.length).toBeGreaterThan(20);
|
||||
expect(out[1].entries[0].blocks?.length).toBeGreaterThanOrEqual(1);
|
||||
@@ -112,9 +125,12 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
{ categoryName: "X", entries: [{ title: "t", body: "b" }] },
|
||||
],
|
||||
};
|
||||
expect(parsePublishedDocumentForCommunityRuleDisplay(doc)).toEqual(
|
||||
doc.sections,
|
||||
);
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out[0]).toEqual(doc.sections[0]);
|
||||
expect(out.map((section) => section.categoryName)).toEqual([
|
||||
"X",
|
||||
"Shared terms",
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces stale document.sections method category with full methodSelections (custom rules)", () => {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DECK_IDS } from "../../lib/create/deckTypes";
|
||||
import type { DeckId, RecommendedCard } from "../../lib/create/deckTypes";
|
||||
import { deckData, recommendDeck } from "../../lib/create/deckRecommend";
|
||||
import { variantsOf } from "../../lib/create/reviewProfiles";
|
||||
import type { ReviewProfile } from "../../lib/create/reviewProfiles";
|
||||
import { methodSlugFromTitle } from "../../lib/create/methodSlugFromTitle";
|
||||
import { resolveMethodId } from "../../lib/create/idAliases";
|
||||
import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json";
|
||||
|
||||
const ROOT = path.resolve(__dirname, "..", "..");
|
||||
|
||||
function readJson<T>(rel: string): T {
|
||||
return JSON.parse(readFileSync(path.join(ROOT, rel), "utf8")) as T;
|
||||
}
|
||||
|
||||
const profiles = readJson<ReviewProfile[]>("data/create/profiles/real-group-profiles.json");
|
||||
const founders = readJson<ReviewProfile[]>("data/create/profiles/one-member-founders.json");
|
||||
|
||||
function compact(card: RecommendedCard) {
|
||||
const reason =
|
||||
card.reason.kind === "added"
|
||||
? `added:${card.reason.answer}`
|
||||
: card.reason.kind === "opened"
|
||||
? `opened:${card.reason.answer}`
|
||||
: card.reason.answer
|
||||
? `fit+${card.reason.answer}`
|
||||
: "fit";
|
||||
return {
|
||||
id: card.id,
|
||||
tag: card.tag,
|
||||
tier: card.tier,
|
||||
reason,
|
||||
...(card.decidedBy ? { decidedBy: card.decidedBy } : {}),
|
||||
...(card.alternative ? { alternative: card.alternative } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function picksFor(set: ReviewProfile[]) {
|
||||
return set.flatMap(variantsOf).map((variant) => ({
|
||||
profile: variant.profile,
|
||||
variant: variant.variant,
|
||||
decks: Object.fromEntries(
|
||||
DECK_IDS.map((deck) => {
|
||||
const result = recommendDeck(deckData(deck), variant.answers);
|
||||
return [
|
||||
deck,
|
||||
{
|
||||
shown: result.shown.map(compact),
|
||||
waiting: result.waiting.map((card) => card.id),
|
||||
},
|
||||
];
|
||||
}),
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
function rowOrderSentence(set: ReviewProfile[]): string {
|
||||
const base = set.flatMap(variantsOf).filter((variant) => variant.variant === "base");
|
||||
const totals = { only: 0, score: 0, otherKinds: 0, rowOrder: 0 };
|
||||
for (const deck of DECK_IDS) {
|
||||
const jobs = deckData(deck).engine.jobs.filter((job) => job.displayTier !== "none");
|
||||
for (const job of jobs) {
|
||||
for (const variant of base) {
|
||||
const winner = recommendDeck(deckData(deck), variant.answers).recommended.find(
|
||||
(card) => card.job === job.id && card.tier !== "answers",
|
||||
);
|
||||
if (winner?.decidedBy) totals[winner.decidedBy] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const picks = totals.only + totals.score + totals.otherKinds + totals.rowOrder;
|
||||
return `Of ${picks} picks for the 92 groups, row order settled ${totals.rowOrder}, other kinds ${totals.otherKinds} and fit ${totals.score}, and ${totals.only} had one eligible card.`;
|
||||
}
|
||||
|
||||
const EXAMPLE_DECK_BY_CATEGORY: Record<string, DeckId> = {
|
||||
Membership: "membership",
|
||||
"Decision-Making": "decisionApproaches",
|
||||
"Decision-making": "decisionApproaches",
|
||||
"Conflict Management": "conflictManagement",
|
||||
Communication: "communication",
|
||||
};
|
||||
|
||||
function chosenCards(documentKey: string): { total: number; ids: string[] } {
|
||||
const doc = (
|
||||
useCasesCompletedRules as Record<
|
||||
string,
|
||||
{ document: { sections: { categoryName: string; entries: { title: string }[] }[] } }
|
||||
>
|
||||
)[documentKey];
|
||||
const live = new Map(
|
||||
DECK_IDS.map((deck) => [deck, new Set(Object.keys(deckData(deck).engine.cards))]),
|
||||
);
|
||||
let total = 0;
|
||||
const ids: string[] = [];
|
||||
for (const section of doc.document.sections) {
|
||||
const deck = EXAMPLE_DECK_BY_CATEGORY[section.categoryName];
|
||||
if (!deck) continue;
|
||||
for (const entry of section.entries) {
|
||||
total += 1;
|
||||
const slug = methodSlugFromTitle(entry.title);
|
||||
const resolved = resolveMethodId(deck, slug);
|
||||
if (resolved && live.get(resolved.deck)?.has(resolved.id)) ids.push(resolved.id);
|
||||
}
|
||||
}
|
||||
return { total, ids };
|
||||
}
|
||||
|
||||
describe("real-group profiles", () => {
|
||||
it("matches the reviewed 2 Nov package run", () => {
|
||||
const expected = readJson<{ picks: unknown[] }>("tests/fixtures/profiles-picks.json");
|
||||
expect(picksFor([...profiles, ...founders])).toEqual(expected.picks);
|
||||
});
|
||||
|
||||
it("reports how often row order settled a pick", () => {
|
||||
const summary = readFileSync(
|
||||
path.join(ROOT, "tests/fixtures/profiles-summary.md"),
|
||||
"utf8",
|
||||
);
|
||||
expect(summary).toContain(rowOrderSentence(profiles));
|
||||
expect(profiles).toHaveLength(92);
|
||||
expect(founders.length).toBeGreaterThan(0);
|
||||
expect(JSON.stringify(profiles)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
|
||||
expect(JSON.stringify(founders)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
|
||||
});
|
||||
|
||||
it("counts how many example-rule cards the engine recommends", () => {
|
||||
const base = profiles.flatMap(variantsOf).filter((variant) => variant.variant === "base");
|
||||
const report: Record<string, { chosen: number; resolved: number; recommendedOnMost: number }> = {};
|
||||
for (const key of ["mutualAidColorado", "foodNotBombs", "boulderCountyStreetMedics"] as const) {
|
||||
const chosen = chosenCards(key);
|
||||
let best = 0;
|
||||
for (const variant of base) {
|
||||
const recommended = new Set<string>();
|
||||
for (const deck of DECK_IDS) {
|
||||
for (const card of recommendDeck(deckData(deck), variant.answers).recommended) {
|
||||
recommended.add(card.id);
|
||||
}
|
||||
}
|
||||
let hit = 0;
|
||||
for (const id of chosen.ids) if (recommended.has(id)) hit += 1;
|
||||
if (hit > best) best = hit;
|
||||
}
|
||||
report[key] = {
|
||||
chosen: chosen.total,
|
||||
resolved: chosen.ids.length,
|
||||
recommendedOnMost: best,
|
||||
};
|
||||
}
|
||||
// The team's own Rule is not in the repo, so this locks the three example
|
||||
// Rules. A change that moves a recommendation changes recommendedOnMost.
|
||||
expect(report).toEqual({
|
||||
mutualAidColorado: { chosen: 7, resolved: 6, recommendedOnMost: 4 },
|
||||
foodNotBombs: { chosen: 8, resolved: 8, recommendedOnMost: 5 },
|
||||
boulderCountyStreetMedics: { chosen: 9, resolved: 8, recommendedOnMost: 5 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,7 +120,7 @@ describe("sectionFromDecision", () => {
|
||||
]);
|
||||
const blocks = section?.entries[0]?.blocks ?? [];
|
||||
expect(blocks).toContainEqual({
|
||||
label: "Consensus Level",
|
||||
label: "Consensus level",
|
||||
body: "100%",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user