mirror of
https://github.com/coder/coder.git
synced 2026-11-14 22:49:03 +08:00
270 lines
7.2 KiB
TypeScript
270 lines
7.2 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import type { Region, User } from "api/typesGenerated";
|
|
import {
|
|
MockAppearanceConfig,
|
|
MockBuildInfo,
|
|
MockEntitlements,
|
|
MockExperiments,
|
|
MockUser,
|
|
} from "testHelpers/entities";
|
|
import {
|
|
DEFAULT_METADATA_KEY,
|
|
type MetadataKey,
|
|
MetadataManager,
|
|
type MetadataValue,
|
|
type RuntimeHtmlMetadata,
|
|
makeUseEmbeddedMetadata,
|
|
useEmbeddedMetadata,
|
|
} from "./useEmbeddedMetadata";
|
|
|
|
// Make sure that no matter what happens in the tests, all metadata is
|
|
// eventually deleted
|
|
const allAppendedNodes = new Set<Set<Element>>();
|
|
afterAll(() => {
|
|
for (const tracker of allAppendedNodes) {
|
|
for (const node of tracker) {
|
|
node.remove();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Using empty array for now, because we don't have a separate mock regions
|
|
// value, but it's still good enough for the tests because it's truthy
|
|
const MockRegions: readonly Region[] = [];
|
|
|
|
const mockDataForTags = {
|
|
appearance: MockAppearanceConfig,
|
|
"build-info": MockBuildInfo,
|
|
entitlements: MockEntitlements,
|
|
experiments: MockExperiments,
|
|
user: MockUser,
|
|
regions: MockRegions,
|
|
} as const satisfies Record<MetadataKey, MetadataValue>;
|
|
|
|
const emptyMetadata: RuntimeHtmlMetadata = {
|
|
appearance: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
"build-info": {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
entitlements: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
experiments: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
regions: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
user: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
};
|
|
|
|
const populatedMetadata: RuntimeHtmlMetadata = {
|
|
appearance: {
|
|
available: true,
|
|
value: MockAppearanceConfig,
|
|
},
|
|
"build-info": {
|
|
available: true,
|
|
value: MockBuildInfo,
|
|
},
|
|
entitlements: {
|
|
available: true,
|
|
value: MockEntitlements,
|
|
},
|
|
experiments: {
|
|
available: true,
|
|
value: MockExperiments,
|
|
},
|
|
regions: {
|
|
available: true,
|
|
value: MockRegions,
|
|
},
|
|
user: {
|
|
available: true,
|
|
value: MockUser,
|
|
},
|
|
};
|
|
|
|
function seedInitialMetadata(metadataKey: string): () => void {
|
|
// Enforcing this to make sure that even if we start to adopt more concurrent
|
|
// tests through Vitest (or similar), there's no risk of global state causing
|
|
// weird, hard-to-test false positives/negatives with other tests
|
|
if (metadataKey === DEFAULT_METADATA_KEY) {
|
|
throw new Error(
|
|
"Please ensure that the key you provide does not match the key used throughout the majority of the application",
|
|
);
|
|
}
|
|
|
|
const trackedNodes = new Set<Element>();
|
|
allAppendedNodes.add(trackedNodes);
|
|
|
|
for (const metadataName in mockDataForTags) {
|
|
// Serializing first because that's the part that can fail; want to fail
|
|
// early if possible
|
|
const value = mockDataForTags[metadataName as keyof typeof mockDataForTags];
|
|
const serialized = JSON.stringify(value);
|
|
|
|
const newNode = document.createElement("meta");
|
|
newNode.setAttribute(metadataKey, metadataName);
|
|
newNode.setAttribute("content", serialized);
|
|
document.head.append(newNode);
|
|
|
|
trackedNodes.add(newNode);
|
|
}
|
|
|
|
return () => {
|
|
for (const node of trackedNodes) {
|
|
node.remove();
|
|
}
|
|
};
|
|
}
|
|
|
|
function renderMetadataHook(metadataKey: string) {
|
|
const manager = new MetadataManager(metadataKey);
|
|
const hook = makeUseEmbeddedMetadata(manager);
|
|
|
|
return {
|
|
...renderHook(hook),
|
|
manager,
|
|
};
|
|
}
|
|
|
|
// Just to be on the safe side, probably want to make sure that each test case
|
|
// is set up with a unique key
|
|
describe(useEmbeddedMetadata.name, () => {
|
|
it("Correctly detects when metadata is missing in the HTML page", () => {
|
|
const key = "cat";
|
|
|
|
// Deliberately avoid seeding any metadata
|
|
const { result } = renderMetadataHook(key);
|
|
expect(result.current.metadata).toEqual<RuntimeHtmlMetadata>(emptyMetadata);
|
|
});
|
|
|
|
it("Can detect when metadata exists in the HTML", () => {
|
|
const key = "dog";
|
|
|
|
const cleanupTags = seedInitialMetadata(key);
|
|
const { result } = renderMetadataHook(key);
|
|
expect(result.current.metadata).toEqual<RuntimeHtmlMetadata>(
|
|
populatedMetadata,
|
|
);
|
|
|
|
cleanupTags();
|
|
});
|
|
|
|
it("Lets external systems (including React) subscribe to when metadata values are deleted", () => {
|
|
const key = "bird";
|
|
const tag1: MetadataKey = "user";
|
|
const tag2: MetadataKey = "appearance";
|
|
|
|
const cleanupTags = seedInitialMetadata(key);
|
|
const { result: reactResult, manager } = renderMetadataHook(key);
|
|
|
|
const nonReactSubscriber = jest.fn();
|
|
manager.subscribe(nonReactSubscriber);
|
|
|
|
const expectedUpdate1: RuntimeHtmlMetadata = {
|
|
...populatedMetadata,
|
|
[tag1]: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
};
|
|
|
|
// Test that updates work when run directly through the metadata manager
|
|
// itself
|
|
act(() => manager.clearMetadataByKey(tag1));
|
|
expect(reactResult.current.metadata).toEqual(expectedUpdate1);
|
|
expect(nonReactSubscriber).toBeCalledWith(expectedUpdate1);
|
|
|
|
nonReactSubscriber.mockClear();
|
|
const expectedUpdate2: RuntimeHtmlMetadata = {
|
|
...expectedUpdate1,
|
|
[tag2]: {
|
|
available: false,
|
|
value: undefined,
|
|
},
|
|
};
|
|
|
|
// Test that updates work when calling the convenience function exposed
|
|
// through the React hooks
|
|
act(() => reactResult.current.clearMetadataByKey(tag2));
|
|
expect(reactResult.current.metadata).toEqual(expectedUpdate2);
|
|
expect(nonReactSubscriber).toBeCalledWith(expectedUpdate2);
|
|
|
|
cleanupTags();
|
|
});
|
|
|
|
it("Will delete the original metadata node when you try deleting a metadata state value", () => {
|
|
const key = "aardvark";
|
|
const tagToDelete: MetadataKey = "user";
|
|
|
|
const cleanupTags = seedInitialMetadata(key);
|
|
const { result } = renderMetadataHook(key);
|
|
|
|
const query = `meta[${key}=${tagToDelete}]`;
|
|
let userNode = document.querySelector(query);
|
|
expect(userNode).not.toBeNull();
|
|
|
|
act(() => result.current.clearMetadataByKey(tagToDelete));
|
|
userNode = document.querySelector(query);
|
|
expect(userNode).toBeNull();
|
|
|
|
cleanupTags();
|
|
});
|
|
|
|
it("Will not notify subscribers if you try deleting a metadata value that does not exist (or has already been deleted)", () => {
|
|
const key = "giraffe";
|
|
const tagToDelete: MetadataKey = "entitlements";
|
|
|
|
const cleanupTags = seedInitialMetadata(key);
|
|
const { result } = renderMetadataHook(key);
|
|
act(() => result.current.clearMetadataByKey(tagToDelete));
|
|
|
|
const resultBeforeNoOp = result.current.metadata;
|
|
act(() => result.current.clearMetadataByKey(tagToDelete));
|
|
expect(result.current.metadata).toBe(resultBeforeNoOp);
|
|
|
|
cleanupTags();
|
|
});
|
|
|
|
// Need to guarantee this, or else we could have a good number of bugs in the
|
|
// React UI
|
|
it("Always treats metadata as immutable values during all deletions", () => {
|
|
const key = "hamster";
|
|
const tagToDelete: MetadataKey = "user";
|
|
|
|
const cleanupTags = seedInitialMetadata(key);
|
|
const { result } = renderMetadataHook(key);
|
|
|
|
const initialResult = result.current.metadata;
|
|
act(() => result.current.clearMetadataByKey(tagToDelete));
|
|
const newResult = result.current.metadata;
|
|
expect(initialResult).not.toBe(newResult);
|
|
|
|
// Mutate the initial result, and make sure the change doesn't propagate to
|
|
// the updated result
|
|
const mutableUser = initialResult.user as {
|
|
available: boolean;
|
|
value: User | undefined;
|
|
};
|
|
|
|
mutableUser.available = false;
|
|
mutableUser.value = undefined;
|
|
expect(mutableUser).toEqual(newResult.user);
|
|
expect(mutableUser).not.toBe(newResult.user);
|
|
|
|
cleanupTags();
|
|
});
|
|
});
|