Skip to content

Commit d5147ac

Browse files
authored
Merge pull request #151 from GraphDone/fix/smoke-gate-determinism-and-lint
Fix CI reds: deterministic grow-flow smoke fixture + remove dead eslint-disable
2 parents bf42215 + 0bc6aab commit d5147ac

2 files changed

Lines changed: 54 additions & 39 deletions

File tree

packages/web/src/pages/Signin.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,6 @@ export function Signin({ initialMagicLink = true }: { initialMagicLink?: boolean
242242
hasEmail: hasEnteredEmail(formData.magicLinkEmail),
243243
});
244244
if (target === 'email') magicLinkEmailRef.current?.focus();
245-
// eslint-disable-next-line react-hooks/exhaustive-deps
246245
}, [useMagicLink, magicLinkSent]);
247246

248247
// Check if guest access is enabled

tests/e2e/smoke/user-smoke.spec.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,47 @@ test.describe('user smoke: the app works from a user point of view @smoke', () =
9797
});
9898

9999
test('grow flow stays healthy: + → empty space → connected named node @smoke', async ({ page }) => {
100+
test.setTimeout(90_000); // fixture build + reload + settle + grow + undo exceed the 30s default
100101
await login(page, TEST_USERS.ADMIN);
101-
// Wait for a graph to auto-load, then for the force layout to SETTLE — the "+"
102-
// grow icon rides on its node, so clicking it while the sim is still moving
103-
// (or while nodes overlap) is the historical source of flake. Poll a node's
104-
// box until it stops moving rather than guessing with a fixed sleep.
105-
await page.locator('.graph-container svg .node').first().waitFor({ timeout: 20000 }).catch(() => {});
102+
await page.waitForTimeout(1500);
103+
104+
// Deterministic fixture: a fresh ADMIN-OWNED graph with ONE regular TASK
105+
// node. The auto-selected default graph is non-deterministic and a known
106+
// flake source — the seeded "Development Team" hierarchy uses sheet nodes
107+
// whose first +-click doesn't enter grow mode, and a Welcome graph owned by
108+
// a different user disables grow. Owning our own single-node graph removes
109+
// both. (API truth is used for the +1/-1 deltas below; viewport culling can't
110+
// skew them.)
111+
const graphId = await page.evaluate(async () => {
112+
const token = localStorage.getItem('authToken') ?? '';
113+
const post = (query: string, variables?: unknown) =>
114+
fetch('/api/graphql', {
115+
method: 'POST',
116+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
117+
body: JSON.stringify({ query, variables }),
118+
}).then((r) => r.json());
119+
const me = await post('{ me { id } }');
120+
const userId = me.data.me.id;
121+
const g = await post(
122+
`mutation($input: [GraphCreateInput!]!) { createGraphs(input: $input) { graphs { id } } }`,
123+
{ input: [{ name: `Grow Smoke ${Date.now()}`, type: 'PROJECT', status: 'ACTIVE', createdBy: userId, isShared: true }] }
124+
);
125+
const gid = g.data.createGraphs.graphs[0].id as string;
126+
await post(
127+
`mutation($input: [WorkItemCreateInput!]!) { createWorkItems(input: $input) { workItems { id } } }`,
128+
{ input: [{ type: 'TASK', title: 'Grow Seed', status: 'IN_PROGRESS', priority: 0.5, positionX: 0, positionY: 0, positionZ: 0, owner: { connect: { where: { node: { id: userId } } } }, graph: { connect: { where: { node: { id: gid } } } } }] }
129+
);
130+
return gid;
131+
});
132+
expect(graphId, 'fixture graph created').toBeTruthy();
133+
134+
await page.evaluate((gid) => localStorage.setItem('currentGraphId', gid), graphId);
135+
await page.reload();
136+
137+
// Wait for the seed node to render, then for the force layout to SETTLE — the
138+
// "+" grow icon rides on its node, so clicking while the sim is still moving
139+
// is the historical source of flake. Poll the node's box until it stops.
140+
await page.locator('.graph-container svg .node').first().waitFor({ timeout: 20000 });
106141
{
107142
let last: { x: number; y: number } | null = null;
108143
let stable = 0;
@@ -120,22 +155,18 @@ test.describe('user smoke: the app works from a user point of view @smoke', () =
120155
}
121156
}
122157

123-
// A graph must be loaded for the grow affordance to exist (the "+" rides on a
124-
// node). Use the DOM for that precondition; use the API for the +1/-1 DELTAS
125-
// below so viewport culling (offscreen nodes aren't in the DOM) can't skew them.
126-
const domNodes = await page.locator('.graph-container svg .node').count();
127-
test.skip(domNodes === 0, 'no graph with nodes auto-selected');
128-
129-
const countAll = () => page.evaluate(async () => {
158+
// Count only THIS fixture graph so the +1 node / +1 edge delta is exact.
159+
const countAll = () => page.evaluate(async (gid) => {
130160
const token = localStorage.getItem('authToken') ?? '';
131161
const res = await fetch('/api/graphql', {
132162
method: 'POST',
133163
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
134-
body: JSON.stringify({ query: `{ workItems { id } edges { id } }` })
164+
body: JSON.stringify({ query: `query($g: ID!) { workItems(where: { graph: { id: $g } }) { id } edges(where: { source: { graph: { id: $g } } }) { id } }`, variables: { g: gid } })
135165
}).then((r) => r.json());
136166
return { nodes: res.data?.workItems?.length ?? -1, edges: res.data?.edges?.length ?? -1 };
137-
});
167+
}, graphId);
138168
const before = await countAll();
169+
expect(before.nodes, 'fixture starts with exactly the seed node').toBe(1);
139170

140171
// Enter grow mode. Retry the click→hint: a settled layout makes this reliable,
141172
// but a stray overlap can still swallow one click, so re-click until grow mode
@@ -182,31 +213,16 @@ test.describe('user smoke: the app works from a user point of view @smoke', () =
182213
await expect.poll(async () => (await countAll()).nodes, { timeout: 10000 }).toBe(before.nodes);
183214
expect((await countAll()).edges, 'undo must remove the created edge').toBe(before.edges);
184215

185-
// Belt-and-braces cleanup in case undo half-failed (keeps re-runnable)
186-
await page.evaluate(async (title) => {
216+
// Tear down the whole fixture graph (edges FIRST — orphan edges break the
217+
// edges query), keeping the suite re-runnable and the DB clean.
218+
await page.evaluate(async (gid) => {
187219
const token = localStorage.getItem('authToken') ?? '';
188-
const find = await fetch('/api/graphql', {
189-
method: 'POST',
190-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
191-
body: JSON.stringify({ query: `query($t: String!) { workItems(where: { title: $t }) { id } }`, variables: { t: title } })
192-
}).then((r) => r.json());
193-
const id = find.data?.workItems?.[0]?.id;
194-
if (!id) return;
195-
// Detach edges FIRST — orphan edges break the whole edges query
196-
await fetch('/api/graphql', {
197-
method: 'POST',
198-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
199-
body: JSON.stringify({
200-
query: `mutation($id: ID!) { deleteEdges(where: { OR: [{ source: { id: $id } }, { target: { id: $id } }] }) { nodesDeleted } }`,
201-
variables: { id }
202-
})
203-
});
204-
await fetch('/api/graphql', {
205-
method: 'POST',
206-
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
207-
body: JSON.stringify({ query: `mutation($id: ID!) { deleteWorkItems(where: { id: $id }) { nodesDeleted } }`, variables: { id } })
208-
});
209-
}, name);
220+
const post = (query: string, variables: unknown) =>
221+
fetch('/api/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ query, variables }) });
222+
await post(`mutation($id: ID!) { deleteEdges(where: { source: { graph: { id: $id } } }) { nodesDeleted } }`, { id: gid });
223+
await post(`mutation($id: ID!) { deleteWorkItems(where: { graph: { id: $id } }) { nodesDeleted } }`, { id: gid });
224+
await post(`mutation($id: ID!) { deleteGraphs(where: { id: $id }) { nodesDeleted } }`, { id: gid });
225+
}, graphId);
210226
});
211227

212228
// A brand-new EMPTY graph (the very first thing a user sees after "Create

0 commit comments

Comments
 (0)