Skip to content
Merged
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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Default owners for the whole repo (will be updated if we grow, but for now this is fine)
* @covenengineering/developers
71 changes: 26 additions & 45 deletions @simulcast/preact/tests/useBroadcast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
import { broadcast, type EventRegistry } from "@simulcast/core";
import { useBroadcast } from "@simulcast/preact";
import { assertStrictEquals } from "@std/assert";
import { mockDOM } from "@test/mockDOM.ts";
import { timeout } from "@test/timeout.ts";
import { type ComponentProps, render, type TargetedMouseEvent } from "preact";
import { cleanup, render, screen } from "@testing-library/preact";
import { userEvent } from "@testing-library/user-event";
import type { ComponentProps, TargetedMouseEvent } from "preact";
import { useState } from "preact/hooks";

const CountComponent = (properties: ComponentProps<"button">) => {
const [count, setCount] = useState(0);

return (
<button
className="add"
onClick={() => setCount(count + 1)}
title="Add"
{...properties}
>
{count}
Expand All @@ -22,7 +22,6 @@ const CountComponent = (properties: ComponentProps<"button">) => {
};

const BroadcastComponent = ({
className,
registry,
state,
...properties
Expand All @@ -40,22 +39,21 @@ const BroadcastComponent = ({

return (
<button
className={className ?? "broadcast"}
onClick={emitClick}
title="Broadcast"
{...properties}
>
Click me!
</button>
);
};

Deno.test.afterEach(cleanup);

Deno.test(
"Broadcast's on handler is called once even when it re-renders",
async () => {
mockDOM({ fakeTimers: true });

const state = { calledTimes: 0 };
const root = document.querySelector("#root") as HTMLDivElement;
const { registry } = broadcast<{
click: TargetedMouseEvent<HTMLButtonElement>;
}>();
Expand All @@ -67,31 +65,24 @@ Deno.test(
<CountComponent />
</>
),
root,
);

const addButton = document.querySelector<HTMLButtonElement>(
"button.add",
) as HTMLButtonElement;
const broadcastButton = document.querySelector<HTMLButtonElement>(
"button.broadcast",
) as HTMLButtonElement;

addButton.click(); // Click button that will re-render once
await timeout(10);
addButton.click(); // Click button that will re-render twice
await timeout(10);
broadcastButton.click(); // Click broadcast button once
const addButton = screen.getByTitle<HTMLButtonElement>("Add");
const broadcastButton = screen.getByTitle<HTMLButtonElement>(
"Broadcast",
);

await userEvent.click(addButton); // Click button that will re-render once
await userEvent.click(addButton); // Click button that will re-render twice
await userEvent.click(broadcastButton); // Click broadcast button once
assertStrictEquals(state.calledTimes, 1); // State should be updated once
assertStrictEquals(addButton.textContent, "2"); // Even when it re-rendered twice
},
);

Deno.test("Broadcast's on handler is removed when unmounted", async () => {
mockDOM({ fakeTimers: true });
const state1 = { calledTimes: 0 };
const state2 = { calledTimes: 0 };
const root = document.querySelector("#root") as HTMLDivElement;
const { registry } = broadcast<{
click: TargetedMouseEvent<HTMLButtonElement>;
}>();
Expand All @@ -111,13 +102,13 @@ Deno.test("Broadcast's on handler is removed when unmounted", async () => {
)
: undefined}
<BroadcastComponent
className="always-visible-broadcast"
state={state2}
title="Always visible Broadcast"
{...{ registry }}
/>
<button
className="toggle"
onClick={() => setVisible(!visible)}
title="Toggle"
type="button"
>
Toggle Visibility
Expand All @@ -126,32 +117,22 @@ Deno.test("Broadcast's on handler is removed when unmounted", async () => {
);
};

render(<App />, root);

await timeout();
render(<App />);

const toggleButton = document.querySelector<HTMLButtonElement>(
"button.toggle",
) as HTMLButtonElement;
const broadcastButton = document.querySelector<HTMLButtonElement>(
"button.broadcast",
) as HTMLButtonElement;
const alwaysVisibleBroadcastButton = document.querySelector<
const toggleButton = screen.getByTitle<HTMLButtonElement>("Toggle");
const broadcastButton = screen.getByTitle<HTMLButtonElement>("Broadcast");
const alwaysVisibleBroadcastButton = screen.getByTitle<
HTMLButtonElement
>(
"button.always-visible-broadcast",
) as HTMLButtonElement;
>("Always visible Broadcast");

await timeout();
// Click broadcast button that will be removed from the DOM
broadcastButton.click();
await userEvent.click(broadcastButton);
// Click broadcast button that will stay in the DOM
alwaysVisibleBroadcastButton.click();
await userEvent.click(alwaysVisibleBroadcastButton);
// Click toggle button (removes the first broadcast button)
toggleButton.click();
await timeout();
await userEvent.click(toggleButton);
// Click broadcast button that stayed again
alwaysVisibleBroadcastButton.click();
await userEvent.click(alwaysVisibleBroadcastButton);
assertStrictEquals(state1.calledTimes, 2); // State 1 should have registered events until removed
assertStrictEquals(state2.calledTimes, 3); // State 2 should have registered all events
});
85 changes: 29 additions & 56 deletions @simulcast/react/tests/useBroadcast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
import { broadcast, type EventRegistry } from "@simulcast/core";
import { useBroadcast } from "@simulcast/react";
import { assertStrictEquals } from "@std/assert";
import { mockDOM } from "@test/mockDOM.ts";
import { timeout } from "@test/timeout.ts";
import { cleanup, render, screen } from "@testing-library/react";
import { userEvent } from "@testing-library/user-event";
import { type ComponentProps, type MouseEvent, useState } from "react";
import { createRoot } from "react-dom/client";

const CountComponent = (properties: ComponentProps<"button">) => {
const [count, setCount] = useState(0);

return (
<button
className="add"
onClick={() => setCount(count + 1)}
title="Add"
{...properties}
>
{count}
Expand All @@ -22,7 +21,6 @@ const CountComponent = (properties: ComponentProps<"button">) => {
};

const BroadcastComponent = ({
className,
registry,
state,
...properties
Expand All @@ -38,29 +36,26 @@ const BroadcastComponent = ({

return (
<button
className={className ?? "broadcast"}
onClick={emitClick}
title="Broadcast"
{...properties}
>
Click me!
</button>
);
};

Deno.test.afterEach(cleanup);

Deno.test(
"Broadcast's on handler is called once even when it re-renders",
async () => {
mockDOM();
const root = createRoot(
document.querySelector("#root") as HTMLDivElement,
);

const state = { calledTimes: 0 };
const { registry } = broadcast<{
click: MouseEvent<HTMLButtonElement>;
}>();

root.render(
render(
(
<>
<BroadcastComponent {...{ registry, state }} />
Expand All @@ -69,30 +64,20 @@ Deno.test(
),
);

await timeout(10);

const addButton = document.querySelector<HTMLButtonElement>(
"button.add",
) as HTMLButtonElement;
const broadcastButton = document.querySelector<HTMLButtonElement>(
"button.broadcast",
) as HTMLButtonElement;

await timeout();
addButton.click(); // Click button that will re-render once
await timeout();
addButton.click(); // Click button that will re-render twice
await timeout();
broadcastButton.click(); // Click broadcast button once
const addButton = screen.getByTitle<HTMLButtonElement>("Add");
const broadcastButton = screen.getByTitle<HTMLButtonElement>(
"Broadcast",
);

await userEvent.click(addButton); // Click button that will re-render once
await userEvent.click(addButton); // Click button that will re-render twice
await userEvent.click(broadcastButton); // Click broadcast button once
assertStrictEquals(state.calledTimes, 1); // State should be updated once
assertStrictEquals(addButton.textContent, "2"); // Even when it re-rendered twice
},
);

Deno.test("Broadcast's on handler is removed when unmounted", async () => {
mockDOM();
const root = createRoot(document.querySelector("#root") as HTMLDivElement);

const state1 = { calledTimes: 0 };
const state2 = { calledTimes: 0 };
const { registry } = broadcast<{
Expand All @@ -108,19 +93,19 @@ Deno.test("Broadcast's on handler is removed when unmounted", async () => {
{visible
? (
<BroadcastComponent
{...{ registry }}
state={state1}
{...{ registry }}
/>
)
: undefined}
<BroadcastComponent
className="always-visible-broadcast"
{...{ registry }}
state={state2}
title="Always visible Broadcast"
{...{ registry }}
/>
<button
className="toggle"
onClick={() => setVisible(!visible)}
title="Toggle"
type="button"
>
Toggle Visibility
Expand All @@ -129,35 +114,23 @@ Deno.test("Broadcast's on handler is removed when unmounted", async () => {
);
};

root.render(<App />);

await timeout(10);
render(<App />);

const toggleButton = document.querySelector<HTMLButtonElement>(
"button.toggle",
) as HTMLButtonElement;
const broadcastButton = document.querySelector<HTMLButtonElement>(
"button.broadcast",
) as HTMLButtonElement;
const alwaysVisibleBroadcastButton = document.querySelector<
const toggleButton = screen.getByTitle<HTMLButtonElement>("Toggle");
const broadcastButton = screen.getByTitle<HTMLButtonElement>("Broadcast");
const alwaysVisibleBroadcastButton = screen.getByTitle<
HTMLButtonElement
>(
"button.always-visible-broadcast",
) as HTMLButtonElement;
>("Always visible Broadcast");

// Click broadcast button that will be removed from the DOM
await timeout();
broadcastButton.click();
await timeout();

await userEvent.click(broadcastButton);
// Click broadcast button that will stay in the DOM
alwaysVisibleBroadcastButton.click();
await timeout();
await userEvent.click(alwaysVisibleBroadcastButton);
// Click toggle button (removes the first broadcast button)
toggleButton.click();
await timeout();
await userEvent.click(toggleButton);
// Click broadcast button that stayed again
alwaysVisibleBroadcastButton.click();
await timeout();
await userEvent.click(alwaysVisibleBroadcastButton);
assertStrictEquals(state1.calledTimes, 2); // State 1 should have registered events until removed
assertStrictEquals(state2.calledTimes, 3); // State 2 should have registered all events
});
8 changes: 5 additions & 3 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
},
"imports": {
"@std/assert": "jsr:@std/assert@^1.0.16",
"@test/": "./test/",
"@testing-library/preact": "npm:@testing-library/preact@^3.2.4",
"@testing-library/react": "npm:@testing-library/react@^16.3.1",
"@testing-library/user-event": "npm:@testing-library/user-event@^14.6.1",
"@types/react": "npm:@types/react@^19.2.7",
"@types/react-dom": "npm:@types/react-dom@^19.2.3",
"@vue/test-utils": "npm:@vue/test-utils@^2.4.6",
Expand Down Expand Up @@ -95,8 +97,8 @@
"nodeModulesDir": "auto",
"tasks": {
"lint-docs": "deno doc --lint ./@coven/**/*.ts ./@simulcast/**/*.ts",
"test": "deno test --allow-env=NODE_ENV --import=./test/setupTests.ts --parallel",
"test-coverage": "deno test --allow-env=NODE_ENV --coverage --doc --import=./test/setupTests.ts --parallel --quiet"
"test": "deno test --ignore-env --import=./setupTests.ts --parallel",
"test-coverage": "deno test --ignore-env --coverage --doc --import=./setupTests.ts --parallel --quiet"
},
"workspace": [
"./@coven/compare",
Expand Down
12 changes: 12 additions & 0 deletions setupTests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// deno-coverage-ignore-file
import { Window } from "happy-dom";

const {
Element, // Required by Vue
Node, // Required by Vue
SVGElement, // Required by Vue
document,
window,
} = new Window();

Object.assign(globalThis, { window, Element, Node, SVGElement, document });
Loading