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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 49 additions & 97 deletions src/browser/contexts/WorkspaceContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@ const createWorkspaceMetadata = (
...overrides,
});

interface WorkspaceMetadataEvent {
workspaceId: string;
metadata: FrontendWorkspaceMetadata | null;
}

type WorkspaceMetadataSubscription = Awaited<ReturnType<APIClient["workspace"]["onMetadata"]>>;

function createSingleMetadataEventStream() {
let resolveEvent: ((event: WorkspaceMetadataEvent) => void) | null = null;

const onMetadata = () =>
Promise.resolve(
(async function* () {
const event = await new Promise<WorkspaceMetadataEvent>((resolve) => {
resolveEvent = resolve;
});
yield event;
})() as unknown as WorkspaceMetadataSubscription
);

return {
onMetadata,
isReady: () => resolveEvent !== null,
emit: (event: WorkspaceMetadataEvent) => {
if (!resolveEvent) {
throw new Error("emit called before metadata subscription started");
}
resolveEvent(event);
},
};
}

describe("WorkspaceContext", () => {
afterEach(() => {
cleanup();
Expand Down Expand Up @@ -85,11 +117,7 @@ describe("WorkspaceContext", () => {
});

test("subscribes to new workspace immediately when metadata event fires", async () => {
const { workspace: workspaceApi } = createMockAPI({
workspace: {
list: () => Promise.resolve([]),
},
});
const { workspace: workspaceApi } = createMockAPI();

await setup();

Expand Down Expand Up @@ -119,28 +147,12 @@ describe("WorkspaceContext", () => {
}),
];

let emitDelete:
| ((event: { workspaceId: string; metadata: FrontendWorkspaceMetadata | null }) => void)
| null = null;
const metadataStream = createSingleMetadataEventStream();

const { workspace: workspaceApi } = createMockAPI({
workspace: {
list: () => Promise.resolve(workspaces),
onMetadata: () =>
Promise.resolve(
(async function* () {
const event = await new Promise<{
workspaceId: string;
metadata: FrontendWorkspaceMetadata | null;
}>((resolve) => {
emitDelete = resolve;
});
yield event;
})() as unknown as Awaited<ReturnType<APIClient["workspace"]["onMetadata"]>>
),
},
projects: {
list: () => Promise.resolve([]),
onMetadata: metadataStream.onMetadata,
},
localStorage: {
[SELECTED_WORKSPACE_KEY]: JSON.stringify({
Expand All @@ -157,10 +169,10 @@ describe("WorkspaceContext", () => {
await waitFor(() => expect(ctx().workspaceMetadata.size).toBe(2));
await waitFor(() => expect(ctx().selectedWorkspace?.workspaceId).toBe(childId));
await waitFor(() => expect(workspaceApi.onMetadata).toHaveBeenCalled());
await waitFor(() => expect(emitDelete).toBeTruthy());
await waitFor(() => expect(metadataStream.isReady()).toBe(true));

act(() => {
emitDelete?.({ workspaceId: childId, metadata: null });
metadataStream.emit({ workspaceId: childId, metadata: null });
});

await waitFor(() => expect(ctx().selectedWorkspace?.workspaceId).toBe(parentId));
Expand All @@ -180,28 +192,12 @@ describe("WorkspaceContext", () => {
}),
];

let emitArchive:
| ((event: { workspaceId: string; metadata: FrontendWorkspaceMetadata | null }) => void)
| null = null;
const metadataStream = createSingleMetadataEventStream();

createMockAPI({
workspace: {
list: () => Promise.resolve(workspaces),
onMetadata: () =>
Promise.resolve(
(async function* () {
const event = await new Promise<{
workspaceId: string;
metadata: FrontendWorkspaceMetadata | null;
}>((resolve) => {
emitArchive = resolve;
});
yield event;
})() as unknown as Awaited<ReturnType<APIClient["workspace"]["onMetadata"]>>
),
},
projects: {
list: () => Promise.resolve([]),
onMetadata: metadataStream.onMetadata,
},
localStorage: {
[SELECTED_WORKSPACE_KEY]: JSON.stringify({
Expand All @@ -216,10 +212,10 @@ describe("WorkspaceContext", () => {
const ctx = await setup();

await waitFor(() => expect(ctx().selectedWorkspace?.workspaceId).toBe(workspaceId));
await waitFor(() => expect(emitArchive).toBeTruthy());
await waitFor(() => expect(metadataStream.isReady()).toBe(true));

act(() => {
emitArchive?.({
metadataStream.emit({
workspaceId,
metadata: createWorkspaceMetadata({
id: workspaceId,
Expand Down Expand Up @@ -259,28 +255,12 @@ describe("WorkspaceContext", () => {
}),
];

let emitArchive:
| ((event: { workspaceId: string; metadata: FrontendWorkspaceMetadata | null }) => void)
| null = null;
const metadataStream = createSingleMetadataEventStream();

createMockAPI({
workspace: {
list: () => Promise.resolve(workspaces),
onMetadata: () =>
Promise.resolve(
(async function* () {
const event = await new Promise<{
workspaceId: string;
metadata: FrontendWorkspaceMetadata | null;
}>((resolve) => {
emitArchive = resolve;
});
yield event;
})() as unknown as Awaited<ReturnType<APIClient["workspace"]["onMetadata"]>>
),
},
projects: {
list: () => Promise.resolve([]),
onMetadata: metadataStream.onMetadata,
},
localStorage: {
[SELECTED_WORKSPACE_KEY]: JSON.stringify({
Expand All @@ -295,7 +275,7 @@ describe("WorkspaceContext", () => {
const ctx = await setup();

await waitFor(() => expect(ctx().selectedWorkspace?.workspaceId).toBe(archivedId));
await waitFor(() => expect(emitArchive).toBeTruthy());
await waitFor(() => expect(metadataStream.isReady()).toBe(true));

const nextSelection = {
workspaceId: nextId,
Expand All @@ -308,7 +288,7 @@ describe("WorkspaceContext", () => {
// The metadata handler must not navigate to the project page after this intent.
act(() => {
ctx().setSelectedWorkspace(nextSelection);
emitArchive?.({
metadataStream.emit({
workspaceId: archivedId,
metadata: createWorkspaceMetadata({
id: archivedId,
Expand Down Expand Up @@ -351,28 +331,12 @@ describe("WorkspaceContext", () => {
}),
];

let emitDelete:
| ((event: { workspaceId: string; metadata: FrontendWorkspaceMetadata | null }) => void)
| null = null;
const metadataStream = createSingleMetadataEventStream();

createMockAPI({
workspace: {
list: () => Promise.resolve(workspaces),
onMetadata: () =>
Promise.resolve(
(async function* () {
const event = await new Promise<{
workspaceId: string;
metadata: FrontendWorkspaceMetadata | null;
}>((resolve) => {
emitDelete = resolve;
});
yield event;
})() as unknown as Awaited<ReturnType<APIClient["workspace"]["onMetadata"]>>
),
},
projects: {
list: () => Promise.resolve([]),
onMetadata: metadataStream.onMetadata,
},
// Parent is selected, not the child
localStorage: {
Expand All @@ -389,11 +353,11 @@ describe("WorkspaceContext", () => {

await waitFor(() => expect(ctx().workspaceMetadata.size).toBe(2));
await waitFor(() => expect(ctx().selectedWorkspace?.workspaceId).toBe(parentId));
await waitFor(() => expect(emitDelete).toBeTruthy());
await waitFor(() => expect(metadataStream.isReady()).toBe(true));

// Delete the non-selected child workspace
act(() => {
emitDelete?.({ workspaceId: childId, metadata: null });
metadataStream.emit({ workspaceId: childId, metadata: null });
});

// Child should be removed from metadata map (this was the bug - it stayed)
Expand Down Expand Up @@ -757,9 +721,6 @@ describe("WorkspaceContext", () => {
}),
]),
},
projects: {
list: () => Promise.resolve([]),
},
server: {
getLaunchProject: () => Promise.resolve("/launch-project"),
},
Expand Down Expand Up @@ -797,9 +758,6 @@ describe("WorkspaceContext", () => {
}),
]),
},
projects: {
list: () => Promise.resolve([]),
},
localStorage: {
selectedWorkspace: JSON.stringify({
workspaceId: "ws-existing",
Expand Down Expand Up @@ -882,9 +840,6 @@ describe("WorkspaceContext", () => {
const projectsListMock = mock(() => Promise.resolve([]));

createMockAPI({
workspace: {
list: () => Promise.resolve([]),
},
projects: {
list: projectsListMock,
},
Expand Down Expand Up @@ -913,9 +868,6 @@ describe("WorkspaceContext", () => {
workspace: {
list: () => Promise.resolve([workspaceWithoutTimestamp]),
},
projects: {
list: () => Promise.resolve([]),
},
});

const ctx = await setup();
Expand Down
27 changes: 4 additions & 23 deletions src/browser/contexts/WorkspaceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,12 +476,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
: null;

if (parentMeta) {
setSelectedWorkspace({
workspaceId: parentMeta.id,
projectPath: parentMeta.projectPath,
projectName: parentMeta.projectName,
namedWorkspacePath: parentMeta.namedWorkspacePath,
});
setSelectedWorkspace(toWorkspaceSelection(parentMeta));
continue;
}

Expand All @@ -498,12 +493,7 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
);

if (fallbackMeta) {
setSelectedWorkspace({
workspaceId: fallbackMeta.id,
projectPath: fallbackMeta.projectPath,
projectName: fallbackMeta.projectName,
namedWorkspacePath: fallbackMeta.namedWorkspacePath,
});
setSelectedWorkspace(toWorkspaceSelection(fallbackMeta));
} else if (projectPath) {
navigateToProject(projectPath);
} else {
Expand Down Expand Up @@ -548,19 +538,10 @@ export function WorkspaceProvider(props: WorkspaceProviderProps) {
// Update metadata immediately to avoid race condition with validation effect
ensureCreatedAt(result.metadata);
seedWorkspaceLocalStorageFromBackend(result.metadata);
setWorkspaceMetadata((prev) => {
const updated = new Map(prev);
updated.set(result.metadata.id, result.metadata);
return updated;
});
setWorkspaceMetadata((prev) => new Map(prev).set(result.metadata.id, result.metadata));

// Return the new workspace selection
return {
projectPath,
projectName: result.metadata.projectName,
namedWorkspacePath: result.metadata.namedWorkspacePath,
workspaceId: result.metadata.id,
};
return toWorkspaceSelection(result.metadata);
} else {
throw new Error(result.error);
}
Expand Down
Loading