From ba0743aaca0dbeda74447e4791e9167018d85ebd Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 14 May 2024 22:43:24 +0200 Subject: [PATCH 01/11] feat(testing): add cleanup method --- .../brisa/src/core/test/api/index.test.tsx | 29 +++++++++++++++- packages/brisa/src/core/test/api/index.ts | 6 ++++ packages/brisa/src/core/test/index.ts | 34 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index ac8373383..d85585956 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -1,5 +1,12 @@ import path from "node:path"; -import { debug, render, serveRoute, waitFor, userEvent } from "@/core/test/api"; +import { + debug, + render, + serveRoute, + waitFor, + userEvent, + cleanup, +} from "@/core/test/api"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { describe, @@ -149,6 +156,26 @@ describe("test api", () => { ); }); + describe("cleanup", () => { + it("should cleanup the registed actions", () => { + globalThis.REGISTERED_ACTIONS = [() => {}]; + cleanup(); + expect(globalThis.REGISTERED_ACTIONS).toBeEmpty(); + }); + + it("should cleanup the document body", async () => { + document.body.innerHTML = "
Foo
"; + cleanup(); + expect(document.body.innerHTML).toBeEmpty(); + }); + + it("should cleanup the document head", async () => { + document.head.innerHTML = "Foo"; + cleanup(); + expect(document.head.innerHTML).toBeEmpty(); + }); + }); + describe("serveRoute", () => { beforeEach(() => { globalThis.mockConstants = { diff --git a/packages/brisa/src/core/test/api/index.ts b/packages/brisa/src/core/test/api/index.ts index 1e2635250..8cc35cbb6 100644 --- a/packages/brisa/src/core/test/api/index.ts +++ b/packages/brisa/src/core/test/api/index.ts @@ -53,6 +53,12 @@ export async function render( return { container, unmount }; } +export function cleanup() { + document.body.innerHTML = ""; + document.head.innerHTML = ""; + globalThis.REGISTERED_ACTIONS = []; +} + /** * Serve a route and return the response */ diff --git a/packages/brisa/src/core/test/index.ts b/packages/brisa/src/core/test/index.ts index c270028b6..c1cee8ac8 100644 --- a/packages/brisa/src/core/test/index.ts +++ b/packages/brisa/src/core/test/index.ts @@ -1,7 +1,41 @@ import { expect } from "bun:test"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; +import { join } from "node:path"; import matchers from "@/core/test/matchers"; +import constants from "@/constants"; +import { transformToWebComponents } from "@/utils/get-client-code-in-page"; +import getWebComponentsList from "@/utils/get-web-components-list"; +import getImportableFilepath from "@/utils/get-importable-filepath"; + +GlobalRegistrator.register(); expect.extend(matchers); + globalThis.REGISTERED_ACTIONS = []; +const { SRC_DIR, LOG_PREFIX } = constants; + +console.log(LOG_PREFIX.INFO, "transforming JSX to web components..."); + +const time = Date.now(); +const webComponentsDir = join(SRC_DIR, "web-components"); +const integrationsPath = getImportableFilepath( + "_integrations", + webComponentsDir, +); +const allWebComponents = await getWebComponentsList(SRC_DIR, integrationsPath); +const res = await transformToWebComponents({ + pagePath: "__tests__", + webComponentsList: allWebComponents, + integrationsPath, + useContextProvider: true, +}); + +if (res) eval(res.code); +if (res?.useI18n) { + // TODO: Implement i18n +} + +console.log(LOG_PREFIX.READY, `transformed in ${Date.now() - time}ms`); + export * from "@/core/test/api"; From b96ddc39dfaf8ff96b7a4c9174c7d83859cc1c4a Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 14 May 2024 22:44:13 +0200 Subject: [PATCH 02/11] docs(testing): add cleanup --- .../src/utils/context-provider/client.tsx | 4 ++-- .../utils/get-client-code-in-page/index.ts | 2 +- .../testing/test-api.md | 21 +++++++++++++++++++ packages/www/style.css | 5 +++-- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/packages/brisa/src/utils/context-provider/client.tsx b/packages/brisa/src/utils/context-provider/client.tsx index 6500a618a..d8febfec2 100644 --- a/packages/brisa/src/utils/context-provider/client.tsx +++ b/packages/brisa/src/utils/context-provider/client.tsx @@ -15,7 +15,7 @@ export default function ClientContextProvider( { children, context, value, pid, cid }: Props, { effect, self, store }: WebContext, ) { - const cId = cid ?? context.id; + const cId = cid ?? context?.id; let pId = pid; if (!pId) { @@ -26,7 +26,7 @@ export default function ClientContextProvider( effect(() => { self.setAttribute("cid", cId); self.setAttribute("pid", pId + ""); - store.set(`context:${cId}:${pId}`, value ?? context.defaultValue); + store.set(`context:${cId}:${pId}`, value ?? context?.defaultValue); }); return children; diff --git a/packages/brisa/src/utils/get-client-code-in-page/index.ts b/packages/brisa/src/utils/get-client-code-in-page/index.ts index 7b3edd320..ced65abfe 100644 --- a/packages/brisa/src/utils/get-client-code-in-page/index.ts +++ b/packages/brisa/src/utils/get-client-code-in-page/index.ts @@ -111,7 +111,7 @@ export default async function getClientCodeInPage({ }; } -async function transformToWebComponents({ +export async function transformToWebComponents({ webComponentsList, useContextProvider, integrationsPath, diff --git a/packages/docs/building-your-application/testing/test-api.md b/packages/docs/building-your-application/testing/test-api.md index fe730159e..78bf02dac 100644 --- a/packages/docs/building-your-application/testing/test-api.md +++ b/packages/docs/building-your-application/testing/test-api.md @@ -140,6 +140,27 @@ Types: serveRoute(route: string): Promise; ``` +## `cleanup` + +Cleans up the document after each test. + +Example: + +```tsx +import { cleanup } from "brisa/test"; +import { afterEach } from "bun:test"; + +afterEach(() => { + cleanup(); +}); +``` + +Types: + +```ts +cleanup(): void; +``` + ## `userEvent` Simulates user events on a target element. diff --git a/packages/www/style.css b/packages/www/style.css index d7c9cffaf..f24433113 100644 --- a/packages/www/style.css +++ b/packages/www/style.css @@ -5431,7 +5431,8 @@ svg.DocSearch-Hit-Select-Icon { svg[data-v-f5c68218] { flex: none; } -.newsletter.container, .newsletter.sponsor { +.newsletter.container, +.newsletter.sponsor { padding: 15px; border-radius: 10px; display: flex; @@ -5454,7 +5455,7 @@ svg[data-v-f5c68218] { display: flex; flex-direction: column; align-items: center; - gap:10px; + gap: 10px; justify-content: center; } From 846e985bec3be273589f37733f79309248c9dd26 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 14 May 2024 22:48:13 +0200 Subject: [PATCH 03/11] docs(testing): improve types --- packages/brisa/src/types/test.d.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/brisa/src/types/test.d.ts b/packages/brisa/src/types/test.d.ts index df7c17651..fbdc77321 100644 --- a/packages/brisa/src/types/test.d.ts +++ b/packages/brisa/src/types/test.d.ts @@ -405,3 +405,23 @@ type userEvent = { * - [Brisa docs](https://brisa.build/building-your-application/testing/test-api#userevent) */ export const userEvent: userEvent; + +/** + * cleanup - Brisa Test API + * + * Cleanup the test environment cleaning up the DOM and other resources. + * + * Example: + * + * ```tsx + * import { cleanup } from "brisa"; + * import { afterEach } from "bun:test"; + * + * afterEach(() => { + * cleanup(); + * }); + * ``` + * + * - [Brisa docs](https://brisa.build/building-your-application/testing/test-api#cleanup) + */ +export function cleanup(): void; \ No newline at end of file From f18e3d7b34f0f19bc0ec82af4fd4ad10cc2d226d Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Tue, 14 May 2024 22:58:51 +0200 Subject: [PATCH 04/11] feat(testing): allow shadowroot on matchers --- .../src/core/test/matchers/index.test.ts | 20 +++++++++++++++++++ .../brisa/src/core/test/matchers/index.ts | 17 ++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/brisa/src/core/test/matchers/index.test.ts b/packages/brisa/src/core/test/matchers/index.test.ts index 6b90dc2d3..ffd118c4a 100644 --- a/packages/brisa/src/core/test/matchers/index.test.ts +++ b/packages/brisa/src/core/test/matchers/index.test.ts @@ -99,6 +99,16 @@ describe("test matchers", () => { expect(fragment).toHaveTextContent("test"); }); + it('should pass if the element is ShadowRoot', () => { + const div = document.createElement('div'); + const shadowRoot = div.attachShadow({ mode: 'open' }); + const span = document.createElement('span'); + span.textContent = 'test'; + shadowRoot.appendChild(span); + + expect(shadowRoot).toHaveTextContent('test'); + }); + it("should fail if the element does not have the rendered text", () => { const div = document.createElement("div"); @@ -123,6 +133,16 @@ describe("test matchers", () => { expect(div).toContainTextContent("test"); }); + it("should pass if the element is ShadowRoot", () => { + const div = document.createElement("div"); + const shadowRoot = div.attachShadow({ mode: "open" }); + const span = document.createElement("span"); + span.textContent = "test"; + shadowRoot.appendChild(span); + + expect(shadowRoot).toContainTextContent("test"); + }); + it("should pass if the element is a documentFragment", () => { const fragment = document.createDocumentFragment(); const div = document.createElement("div"); diff --git a/packages/brisa/src/core/test/matchers/index.ts b/packages/brisa/src/core/test/matchers/index.ts index ef0395ae5..ab81bda7f 100644 --- a/packages/brisa/src/core/test/matchers/index.ts +++ b/packages/brisa/src/core/test/matchers/index.ts @@ -39,11 +39,13 @@ function toHaveTagName(received: unknown, tagName: string) { }; } -function toHaveTextContent(received: unknown, text: string) { - const isValidElement = - received instanceof HTMLElement || received instanceof DocumentFragment; +function isValidHTMLElement(element: unknown): element is HTMLElement { + return typeof (element as any)?.textContent === "string" +} + - if (!isValidElement) { +function toHaveTextContent(received: unknown, text: string) { + if (!isValidHTMLElement(received)) { throw new Error( "Invalid usage of toHaveTextContent(received, text). The argument received should be an HTMLElement", ); @@ -56,11 +58,8 @@ function toHaveTextContent(received: unknown, text: string) { } function toContainTextContent(received: unknown, text: string) { - const isValidElement = - received instanceof HTMLElement || received instanceof DocumentFragment; - - if (!isValidElement) { - throw new Error( + if (!isValidHTMLElement(received)) { + throw new Error( "Invalid usage of toContainTextContent(received, text). The argument received should be an HTMLElement", ); } From f271196eb736fdfd142521985f37b14bbab332e5 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Wed, 15 May 2024 00:54:33 +0200 Subject: [PATCH 05/11] test(testing): test custom counter with Brisa test api --- .../web-components/custom-counter.tsx | 16 ++++ .../brisa/src/core/test/api/index.test.tsx | 80 +++++++++++++++++-- packages/brisa/src/core/test/index.ts | 33 +------- .../src/core/test/matchers/index.test.ts | 12 +-- .../brisa/src/core/test/matchers/index.ts | 7 +- .../src/core/test/run-web-components/index.ts | 46 +++++++++++ packages/brisa/src/types/test.d.ts | 12 +-- .../get-web-components-list/index.test.ts | 15 ++++ 8 files changed, 168 insertions(+), 53 deletions(-) create mode 100644 packages/brisa/src/__fixtures__/web-components/custom-counter.tsx create mode 100644 packages/brisa/src/core/test/run-web-components/index.ts diff --git a/packages/brisa/src/__fixtures__/web-components/custom-counter.tsx b/packages/brisa/src/__fixtures__/web-components/custom-counter.tsx new file mode 100644 index 000000000..3eba96fba --- /dev/null +++ b/packages/brisa/src/__fixtures__/web-components/custom-counter.tsx @@ -0,0 +1,16 @@ +import type { WebContext } from "brisa"; + +export default function Counter( + { initialValue = 0 }: { initialValue: number }, + { state }: WebContext, +) { + const count = state(initialValue); + + return ( +
+ + {count.value} + +
+ ); +} diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index d85585956..5f4139a70 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -141,14 +141,82 @@ describe("test api", () => { expect(mockLog).toHaveBeenCalledWith("foo"); }); - it.todo("should render a web component", async () => {}); + it("should render a web component", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(globalThis.mockConstants as any); - it.todo("should render a web component with props", async () => {}); + // @ts-ignore + const { container } = await render(); + const customCounter = + container.querySelector("custom-counter")!.shadowRoot!; - it.todo( - "should be possible to interact with a web component", - async () => {}, - ); + expect(customCounter).toContainTextContent("0"); + }); + + it.todo("should render a web component with props", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(globalThis.mockConstants as any); + + // @ts-ignore + const { container } = await render(); + const customCounter = + container.querySelector("custom-counter")!.shadowRoot!; + + expect(customCounter.innerHTML).toBe("5"); + }); + + it("should be possible to interact with a web component", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(globalThis.mockConstants as any); + + // @ts-ignore + const { container } = await render(); + const customCounter = + container.querySelector("custom-counter")!.shadowRoot!; + const [increment, decrement] = customCounter.querySelectorAll("button"); + + expect(customCounter).toContainTextContent("0"); + + userEvent.click(increment); + + expect(customCounter).toContainTextContent("1"); + + userEvent.click(increment); + + expect(customCounter).toContainTextContent("2"); + + userEvent.click(decrement); + + expect(customCounter).toContainTextContent("1"); + + userEvent.click(decrement); + + expect(customCounter).toContainTextContent("0"); + }); it.todo( "should be possible to render a server component with a web component inside", diff --git a/packages/brisa/src/core/test/index.ts b/packages/brisa/src/core/test/index.ts index c1cee8ac8..bc3149e4e 100644 --- a/packages/brisa/src/core/test/index.ts +++ b/packages/brisa/src/core/test/index.ts @@ -1,41 +1,12 @@ import { expect } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; -import { join } from "node:path"; import matchers from "@/core/test/matchers"; import constants from "@/constants"; -import { transformToWebComponents } from "@/utils/get-client-code-in-page"; -import getWebComponentsList from "@/utils/get-web-components-list"; -import getImportableFilepath from "@/utils/get-importable-filepath"; +import runWebComponents from "@/core/test/run-web-components"; GlobalRegistrator.register(); - expect.extend(matchers); - globalThis.REGISTERED_ACTIONS = []; - -const { SRC_DIR, LOG_PREFIX } = constants; - -console.log(LOG_PREFIX.INFO, "transforming JSX to web components..."); - -const time = Date.now(); -const webComponentsDir = join(SRC_DIR, "web-components"); -const integrationsPath = getImportableFilepath( - "_integrations", - webComponentsDir, -); -const allWebComponents = await getWebComponentsList(SRC_DIR, integrationsPath); -const res = await transformToWebComponents({ - pagePath: "__tests__", - webComponentsList: allWebComponents, - integrationsPath, - useContextProvider: true, -}); - -if (res) eval(res.code); -if (res?.useI18n) { - // TODO: Implement i18n -} - -console.log(LOG_PREFIX.READY, `transformed in ${Date.now() - time}ms`); +await runWebComponents(constants); export * from "@/core/test/api"; diff --git a/packages/brisa/src/core/test/matchers/index.test.ts b/packages/brisa/src/core/test/matchers/index.test.ts index ffd118c4a..16b3d54c4 100644 --- a/packages/brisa/src/core/test/matchers/index.test.ts +++ b/packages/brisa/src/core/test/matchers/index.test.ts @@ -99,14 +99,14 @@ describe("test matchers", () => { expect(fragment).toHaveTextContent("test"); }); - it('should pass if the element is ShadowRoot', () => { - const div = document.createElement('div'); - const shadowRoot = div.attachShadow({ mode: 'open' }); - const span = document.createElement('span'); - span.textContent = 'test'; + it("should pass if the element is ShadowRoot", () => { + const div = document.createElement("div"); + const shadowRoot = div.attachShadow({ mode: "open" }); + const span = document.createElement("span"); + span.textContent = "test"; shadowRoot.appendChild(span); - expect(shadowRoot).toHaveTextContent('test'); + expect(shadowRoot).toHaveTextContent("test"); }); it("should fail if the element does not have the rendered text", () => { diff --git a/packages/brisa/src/core/test/matchers/index.ts b/packages/brisa/src/core/test/matchers/index.ts index ab81bda7f..b16f69ab1 100644 --- a/packages/brisa/src/core/test/matchers/index.ts +++ b/packages/brisa/src/core/test/matchers/index.ts @@ -40,10 +40,9 @@ function toHaveTagName(received: unknown, tagName: string) { } function isValidHTMLElement(element: unknown): element is HTMLElement { - return typeof (element as any)?.textContent === "string" + return typeof (element as any)?.textContent === "string"; } - function toHaveTextContent(received: unknown, text: string) { if (!isValidHTMLElement(received)) { throw new Error( @@ -58,8 +57,8 @@ function toHaveTextContent(received: unknown, text: string) { } function toContainTextContent(received: unknown, text: string) { - if (!isValidHTMLElement(received)) { - throw new Error( + if (!isValidHTMLElement(received)) { + throw new Error( "Invalid usage of toContainTextContent(received, text). The argument received should be an HTMLElement", ); } diff --git a/packages/brisa/src/core/test/run-web-components/index.ts b/packages/brisa/src/core/test/run-web-components/index.ts new file mode 100644 index 000000000..bb4036ba8 --- /dev/null +++ b/packages/brisa/src/core/test/run-web-components/index.ts @@ -0,0 +1,46 @@ +import { join } from "node:path"; +import fs from "node:fs"; +import constants from "@/constants"; +import { transformToWebComponents } from "@/utils/get-client-code-in-page"; +import getWebComponentsList from "@/utils/get-web-components-list"; +import getImportableFilepath from "@/utils/get-importable-filepath"; + +// TODO: add test about this +// TODO: not log and early return if there is no web components (test it) +export default async function runWebComponents({ + SRC_DIR, + BUILD_DIR, + LOG_PREFIX, +}: typeof constants) { + console.log(LOG_PREFIX.INFO, "transforming JSX to web components..."); + + const time = Date.now(); + const webComponentsDir = join(SRC_DIR, "web-components"); + const internalBrisaFolder = join(BUILD_DIR, "_brisa"); + + if (!fs.existsSync(internalBrisaFolder)) { + fs.mkdirSync(internalBrisaFolder, { recursive: true }); + } + + const integrationsPath = getImportableFilepath( + "_integrations", + webComponentsDir, + ); + const allWebComponents = await getWebComponentsList( + SRC_DIR, + integrationsPath, + ); + const res = await transformToWebComponents({ + pagePath: "__tests__", + webComponentsList: allWebComponents, + integrationsPath, + useContextProvider: true, + }); + + if (res) eval(res.code); + if (res?.useI18n) { + // TODO: Implement i18n + } + + console.log(LOG_PREFIX.READY, `transformed in ${Date.now() - time}ms`); +} diff --git a/packages/brisa/src/types/test.d.ts b/packages/brisa/src/types/test.d.ts index fbdc77321..4349c8ef3 100644 --- a/packages/brisa/src/types/test.d.ts +++ b/packages/brisa/src/types/test.d.ts @@ -408,20 +408,20 @@ export const userEvent: userEvent; /** * cleanup - Brisa Test API - * + * * Cleanup the test environment cleaning up the DOM and other resources. - * + * * Example: - * + * * ```tsx * import { cleanup } from "brisa"; * import { afterEach } from "bun:test"; - * + * * afterEach(() => { * cleanup(); * }); * ``` - * + * * - [Brisa docs](https://brisa.build/building-your-application/testing/test-api#cleanup) */ -export function cleanup(): void; \ No newline at end of file +export function cleanup(): void; diff --git a/packages/brisa/src/utils/get-web-components-list/index.test.ts b/packages/brisa/src/utils/get-web-components-list/index.test.ts index c4d548d92..35565e600 100644 --- a/packages/brisa/src/utils/get-web-components-list/index.test.ts +++ b/packages/brisa/src/utils/get-web-components-list/index.test.ts @@ -29,6 +29,11 @@ describe("utils", () => { const result = await getWebComponentsList(fixturesDir); expect(result).toEqual({ + "custom-counter": path.join( + fixturesDir, + "web-components", + "custom-counter.tsx", + ), "native-some-example": path.join( fixturesDir, "web-components", @@ -59,6 +64,11 @@ describe("utils", () => { const result = await getWebComponentsList(fixturesDir, integrationsPath); expect(result).toEqual({ + "custom-counter": path.join( + fixturesDir, + "web-components", + "custom-counter.tsx", + ), "foo-component": path.join(fixturesDir, "lib", "foo.tsx"), "native-some-example": path.join( fixturesDir, @@ -90,6 +100,11 @@ describe("utils", () => { const result = await getWebComponentsList(fixturesDir, integrationsPath); expect(result).toEqual({ + "custom-counter": path.join( + fixturesDir, + "web-components", + "custom-counter.tsx", + ), "native-some-example": path.join( fixturesDir, "web-components", From c04ff5d0a710b85d7303aa1bbc0dac5f4105e497 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Wed, 15 May 2024 14:27:02 +0200 Subject: [PATCH 06/11] feat(testing): modify debug attribute --- .../brisa/src/core/test/api/index.test.tsx | 70 ++++++++++++++++++- packages/brisa/src/core/test/api/index.ts | 57 +++++++++------ packages/brisa/src/types/test.d.ts | 15 +++- .../testing/test-api.md | 22 +++++- 4 files changed, 137 insertions(+), 27 deletions(-) diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index 5f4139a70..cd5da4dd2 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -176,11 +176,37 @@ describe("test api", () => { // @ts-ignore const { container } = await render(); const customCounter = - container.querySelector("custom-counter")!.shadowRoot!; + container.querySelector("custom-counter")?.shadowRoot!; expect(customCounter.innerHTML).toBe("5"); }); + it.todo("should render a web component with slots", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(globalThis.mockConstants as any); + + const { container } = await render( + // @ts-ignore + +
Header
+
Footer
+ {/* @ts-ignore */} +
, + ); + const customSlot = container.querySelector("custom-slot")?.shadowRoot!; + + expect(customSlot).toContainTextContent("Header"); + expect(customSlot).toContainTextContent("Footer"); + }); + it("should be possible to interact with a web component", async () => { globalThis.mockConstants = { ...(getConstants() ?? {}), @@ -341,7 +367,7 @@ describe("test api", () => { "\n " + blueLog("") + - "\n " + + "\n " + "Foo\n " + blueLog("") + "\n " + @@ -365,6 +391,46 @@ describe("test api", () => { greenLog('"test"'), ); }); + + it("should be possible to log a shadow root", () => { + const mockLog = spyOn(console, "log"); + const shadowRoot = document + .createElement("div") + .attachShadow({ mode: "open" }); + shadowRoot.innerHTML = "
Foo
"; + debug(shadowRoot); + expect(mockLog.mock.calls[0][0]).toBe( + blueLog("") + "\n " + "Foo\n" + blueLog(""), + ); + }); + + it("should be possible to log a document fragment", () => { + const mockLog = spyOn(console, "log"); + const fragment = document.createDocumentFragment(); + const div = document.createElement("div"); + div.innerHTML = "Foo
Bar
"; + fragment.appendChild(div); + debug(fragment); + expect(mockLog.mock.calls[0][0]).toBe( + blueLog("") + + "\n " + + "Foo\n " + + blueLog("") + + "\n " + + "Bar\n " + + blueLog("") + + "\n" + + blueLog(""), + ); + }); + + it("should be possible to log null element an see an empty fragment", () => { + const mockLog = spyOn(console, "log"); + debug(null); + expect(mockLog.mock.calls[0][0]).toBe(blueLog("<>\n")); + }); }); describe("userEvent", () => { diff --git a/packages/brisa/src/core/test/api/index.ts b/packages/brisa/src/core/test/api/index.ts index 8cc35cbb6..d9051cbd2 100644 --- a/packages/brisa/src/core/test/api/index.ts +++ b/packages/brisa/src/core/test/api/index.ts @@ -99,41 +99,58 @@ export async function waitFor(fn: () => unknown, maxMilliseconds = 1000) { /** * Debug the current DOM */ -export function debug() { - console.log(prettyDOM(document.documentElement)); +export function debug( + element: + | HTMLElement + | DocumentFragment + | ShadowRoot + | null = document.documentElement, +) { + console.log(element ? prettyDOM(element) : blueLog("<>\n")); } -function prettyDOM(element: HTMLElement, prefix: string = ""): any { +function prettyDOM( + element: HTMLElement | DocumentFragment | ShadowRoot, + prefix: string = "", +): any { + const isAnElement = isElement(element); + const nextPrefix = !prefix && !isAnElement ? "" : prefix + " "; + const separator = nextPrefix ? "\n" : ""; const lines = []; - const attrs = element.attributes; - lines.push(prefix, blueLog("<" + element.localName)); - - for (let i = 0; i < attrs.length; i += 1) { - const attr = attrs[i]; - lines.push( - "\n", - prefix, - " ", - cyanLog(attr.name), - `=${greenLog('"' + attr.value + '"')}`, - ); - } - lines.push(blueLog(">")); + if (isAnElement) { + const attrs = element.attributes; + lines.push(prefix, blueLog("<" + element.localName)); + + for (let i = 0; i < attrs.length; i += 1) { + const attr = attrs[i]; + lines.push( + separator, + prefix, + " ", + cyanLog(attr.name), + `=${greenLog('"' + attr.value + '"')}`, + ); + } + + lines.push(blueLog(">")); + } let child = isTemplate(element) ? element.content.firstChild : element.firstChild; while (child) { if (isElement(child)) { - lines.push("\n", prettyDOM(child, prefix + " ")); + lines.push(separator, prettyDOM(child, nextPrefix)); } else { - lines.push("\n", prefix, child.textContent); + lines.push(separator, prefix + " ", child.textContent); } child = child.nextSibling; } - lines.push("\n", prefix, blueLog(``)); + if (isAnElement) { + lines.push(separator, prefix, blueLog(``)); + } return lines.join(""); } diff --git a/packages/brisa/src/types/test.d.ts b/packages/brisa/src/types/test.d.ts index 4349c8ef3..cbd28a53a 100644 --- a/packages/brisa/src/types/test.d.ts +++ b/packages/brisa/src/types/test.d.ts @@ -99,12 +99,21 @@ export async function waitFor( * Example: * * ```tsx - * import { debug } from "brisa"; + * import { debug, render } from "brisa"; * * await render(
Hello World
); * debug(); * ``` * + * Also you can pass an element to debug: + * + * ```tsx + * import { debug, render } from "brisa"; + * + * await render(
Hello World
); + * debug(document.querySelector("div")); + * ``` + * * In the console you will see: * * ```html @@ -119,7 +128,9 @@ export async function waitFor( * * - [Brisa docs](https://brisa.build/building-your-application/testing/test-api#debug) */ -export function debug(): void; +export function debug( + element?: HTMLElement | DocumentFragment | ShadowRoot | null, +): void; type userEvent = { /** diff --git a/packages/docs/building-your-application/testing/test-api.md b/packages/docs/building-your-application/testing/test-api.md index 78bf02dac..87a7e5ece 100644 --- a/packages/docs/building-your-application/testing/test-api.md +++ b/packages/docs/building-your-application/testing/test-api.md @@ -300,9 +300,25 @@ Example: import { render, debug } from "brisa/test"; import { test, expect } from "bun:test"; -test("debug", async () => { +test("debug a specific element", async () => { const { container } = await render(); - debug(); + debug(container); +}); +``` + +If no element is passed, it will debug the entire document. + +Example: + +```tsx +import { debug, render, serveRoute } from "brisa/test"; +import { test } from "bun:test"; + +test("debug all the document", async () => { + const pageResponse = await serveRoute("/about"); + await render(pageResponse); + + debug(); // Debug the entire document }); ``` @@ -319,5 +335,5 @@ In the console, you will see the HTML of the document in a readable format: Types: ```ts -debug(): void; +debug(element?: HTMLElement | DocumentFragment | ShadowRoot | null): void; ``` From a542ab7cbf084ba6f51abdb08596b84b818423b6 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Wed, 15 May 2024 22:52:49 +0200 Subject: [PATCH 07/11] feat(testing): add toHaveElementByNodeName matcher --- .../web-components/custom-slot.tsx | 5 +++++ .../brisa/src/core/test/api/index.test.tsx | 7 ++++--- .../brisa/src/core/test/matchers/index.test.ts | 18 ++++++++++++++++++ packages/brisa/src/core/test/matchers/index.ts | 11 +++++++++++ packages/brisa/src/types/index.d.ts | 12 ++++++++++++ .../get-web-components-list/index.test.ts | 15 +++++++++++++++ .../testing/matchers.md | 17 +++++++++++++++++ 7 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 packages/brisa/src/__fixtures__/web-components/custom-slot.tsx diff --git a/packages/brisa/src/__fixtures__/web-components/custom-slot.tsx b/packages/brisa/src/__fixtures__/web-components/custom-slot.tsx new file mode 100644 index 000000000..9d80dfc9e --- /dev/null +++ b/packages/brisa/src/__fixtures__/web-components/custom-slot.tsx @@ -0,0 +1,5 @@ +import type { WebContext } from "brisa"; + +export default function Counter({ children }: { children: JSX.Element }) { + return
{children}
; +} diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index cd5da4dd2..6ce4942fc 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -181,7 +181,7 @@ describe("test api", () => { expect(customCounter.innerHTML).toBe("5"); }); - it.todo("should render a web component with slots", async () => { + it("should render a web component with slots", async () => { globalThis.mockConstants = { ...(getConstants() ?? {}), SRC_DIR: BUILD_DIR, @@ -203,8 +203,9 @@ describe("test api", () => { ); const customSlot = container.querySelector("custom-slot")?.shadowRoot!; - expect(customSlot).toContainTextContent("Header"); - expect(customSlot).toContainTextContent("Footer"); + expect(customSlot).toHaveElementByNodeName("slot"); + expect(container).toContainTextContent("Header"); + expect(container).toContainTextContent("Footer"); }); it("should be possible to interact with a web component", async () => { diff --git a/packages/brisa/src/core/test/matchers/index.test.ts b/packages/brisa/src/core/test/matchers/index.test.ts index 16b3d54c4..7cfb7e5de 100644 --- a/packages/brisa/src/core/test/matchers/index.test.ts +++ b/packages/brisa/src/core/test/matchers/index.test.ts @@ -515,4 +515,22 @@ describe("test matchers", () => { ); }); }); + + describe("toHaveElementByNodeName", () => { + it("should pass if the element has an element with the given node name", () => { + const div = document.createElement("div"); + const span = document.createElement("span"); + div.appendChild(span); + + expect(div).toHaveElementByNodeName("span"); + }); + + it("should fail if the element does not have an element with the given node name", () => { + const div = document.createElement("div"); + + expect(() => expect(div).toHaveElementByNodeName("span")).toThrowError( + "expected element to have span", + ); + }); + }); }); diff --git a/packages/brisa/src/core/test/matchers/index.ts b/packages/brisa/src/core/test/matchers/index.ts index b16f69ab1..092f965c6 100644 --- a/packages/brisa/src/core/test/matchers/index.ts +++ b/packages/brisa/src/core/test/matchers/index.ts @@ -244,6 +244,16 @@ function toBeInTheDocument(received: unknown) { }; } +function toHaveElementByNodeName( + received: HTMLElement | DocumentFragment | ShadowRoot, + elementName: string, +) { + return { + pass: received.querySelector(elementName) !== null, + message: () => `expected element to have ${elementName}`, + }; +} + export default { toBeChecked, toHaveAttribute, @@ -263,4 +273,5 @@ export default { toBeInvalid, toBeInputTypeOf, toBeInTheDocument, + toHaveElementByNodeName, }; diff --git a/packages/brisa/src/types/index.d.ts b/packages/brisa/src/types/index.d.ts index d7ed52191..77a9e71c7 100644 --- a/packages/brisa/src/types/index.d.ts +++ b/packages/brisa/src/types/index.d.ts @@ -9797,6 +9797,18 @@ export interface BrisaTestMatchers { * @see [More details](https://brisa.build/building-your-application/testing/matchers#tobeinvalid) */ toBeInTheDocument(): void; + + /** + * Use `toHaveElementByNodeName` to assert that an element has a specific node name. + * + * Example: + * + * ```ts + * expect(element).toHaveElementByNodeName('div'); + * + * @see [More details](https://brisa.build/building-your-application/testing/matchers#tohaveelementbyname) + */ + toHaveElementByNodeName(name: string): void; } declare module "bun:test" { diff --git a/packages/brisa/src/utils/get-web-components-list/index.test.ts b/packages/brisa/src/utils/get-web-components-list/index.test.ts index 35565e600..124907693 100644 --- a/packages/brisa/src/utils/get-web-components-list/index.test.ts +++ b/packages/brisa/src/utils/get-web-components-list/index.test.ts @@ -34,6 +34,11 @@ describe("utils", () => { "web-components", "custom-counter.tsx", ), + "custom-slot": path.join( + fixturesDir, + "web-components", + "custom-slot.tsx", + ), "native-some-example": path.join( fixturesDir, "web-components", @@ -69,6 +74,11 @@ describe("utils", () => { "web-components", "custom-counter.tsx", ), + "custom-slot": path.join( + fixturesDir, + "web-components", + "custom-slot.tsx", + ), "foo-component": path.join(fixturesDir, "lib", "foo.tsx"), "native-some-example": path.join( fixturesDir, @@ -105,6 +115,11 @@ describe("utils", () => { "web-components", "custom-counter.tsx", ), + "custom-slot": path.join( + fixturesDir, + "web-components", + "custom-slot.tsx", + ), "native-some-example": path.join( fixturesDir, "web-components", diff --git a/packages/docs/building-your-application/testing/matchers.md b/packages/docs/building-your-application/testing/matchers.md index d8a236fae..7086b17ad 100644 --- a/packages/docs/building-your-application/testing/matchers.md +++ b/packages/docs/building-your-application/testing/matchers.md @@ -310,6 +310,23 @@ Types: toBeInTheDocument(): void; ``` +## `toHaveElementByNodeName` + +Verifies the presence of an element with a specific node name. + +Example: + +```ts +expect(element).toHaveElementByNodeName("div"); +expect(element).not.toHaveElementByNodeName("span"); +``` + +Types: + +```ts +toHaveElementByNodeName(nodeName: string): void; +``` + ## More Matchers from Bun In addition to the custom matchers provided by Brisa, you can also use the default matchers from Bun, such as `toBe`, `toEqual`, `toBeTruthy`, `toBeFalsy`... From f015ece8b5995daf686a85b9ea0e84c7b7f875ae Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Wed, 15 May 2024 23:08:52 +0200 Subject: [PATCH 08/11] test(testing): add test cases in runWebComponents --- .../test/run-web-components/index.test.ts | 51 +++++++++++++++++++ .../src/core/test/run-web-components/index.ts | 30 +++++------ 2 files changed, 65 insertions(+), 16 deletions(-) create mode 100644 packages/brisa/src/core/test/run-web-components/index.test.ts diff --git a/packages/brisa/src/core/test/run-web-components/index.test.ts b/packages/brisa/src/core/test/run-web-components/index.test.ts new file mode 100644 index 000000000..5eea775dc --- /dev/null +++ b/packages/brisa/src/core/test/run-web-components/index.test.ts @@ -0,0 +1,51 @@ +import { expect, describe, it, beforeEach, afterEach, spyOn } from "bun:test"; +import constants, { getConstants } from "@/constants"; +import { join } from "node:path"; +import { GlobalRegistrator } from "@happy-dom/global-registrator"; +import runWebComponents from "@/core/test/run-web-components"; + +const BUILD_DIR = join(import.meta.dir, "..", "..", "..", "__fixtures__"); + +describe("runWebComponents", () => { + beforeEach(() => { + GlobalRegistrator.register(); + globalThis.mockConstants = { + ...getConstants(), + SRC_DIR: BUILD_DIR, + BUILD_DIR: BUILD_DIR, + }; + }); + afterEach(() => { + globalThis.mockConstants = undefined; + GlobalRegistrator.unregister(); + }); + + it("should transform JSX to web components and define them to the document", async () => { + expect(customElements.get("custom-counter")).not.toBeDefined(); + expect(customElements.get("custom-slot")).not.toBeDefined(); + expect(customElements.get("web-component")).not.toBeDefined(); + expect(customElements.get("native-some-example")).not.toBeDefined(); + expect(customElements.get("with-context")).not.toBeDefined(); + expect(customElements.get("with-link")).not.toBeDefined(); + expect(customElements.get("foo-component")).not.toBeDefined(); + await runWebComponents(); + expect(customElements.get("custom-counter")).toBeDefined(); + expect(customElements.get("custom-slot")).toBeDefined(); + expect(customElements.get("web-component")).toBeDefined(); + expect(customElements.get("native-some-example")).toBeDefined(); + expect(customElements.get("with-context")).toBeDefined(); + expect(customElements.get("with-link")).toBeDefined(); + expect(customElements.get("foo-component")).toBeDefined(); + }); + + it("should NOT log and early return if there is no web components", async () => { + const logSpy = spyOn(console, "log"); + globalThis.mockConstants = { + ...getConstants(), + SRC_DIR: join(BUILD_DIR, "no-web-components"), + BUILD_DIR: join(BUILD_DIR, "no-web-components"), + }; + await runWebComponents(); + expect(logSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/brisa/src/core/test/run-web-components/index.ts b/packages/brisa/src/core/test/run-web-components/index.ts index bb4036ba8..bad22bd66 100644 --- a/packages/brisa/src/core/test/run-web-components/index.ts +++ b/packages/brisa/src/core/test/run-web-components/index.ts @@ -1,27 +1,14 @@ import { join } from "node:path"; import fs from "node:fs"; -import constants from "@/constants"; +import { getConstants } from "@/constants"; import { transformToWebComponents } from "@/utils/get-client-code-in-page"; import getWebComponentsList from "@/utils/get-web-components-list"; import getImportableFilepath from "@/utils/get-importable-filepath"; -// TODO: add test about this -// TODO: not log and early return if there is no web components (test it) -export default async function runWebComponents({ - SRC_DIR, - BUILD_DIR, - LOG_PREFIX, -}: typeof constants) { - console.log(LOG_PREFIX.INFO, "transforming JSX to web components..."); - - const time = Date.now(); +export default async function runWebComponents() { + const { LOG_PREFIX, SRC_DIR, BUILD_DIR } = getConstants(); const webComponentsDir = join(SRC_DIR, "web-components"); const internalBrisaFolder = join(BUILD_DIR, "_brisa"); - - if (!fs.existsSync(internalBrisaFolder)) { - fs.mkdirSync(internalBrisaFolder, { recursive: true }); - } - const integrationsPath = getImportableFilepath( "_integrations", webComponentsDir, @@ -30,6 +17,17 @@ export default async function runWebComponents({ SRC_DIR, integrationsPath, ); + + if (Object.keys(allWebComponents).length === 0) return; + + console.log(LOG_PREFIX.INFO, "transforming JSX to web components..."); + + const time = Date.now(); + + if (!fs.existsSync(internalBrisaFolder)) { + fs.mkdirSync(internalBrisaFolder, { recursive: true }); + } + const res = await transformToWebComponents({ pagePath: "__tests__", webComponentsList: allWebComponents, From 127a345d8c28cbeb339e942a47d026c7add02d78 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 16 May 2024 00:37:05 +0200 Subject: [PATCH 09/11] feat(testing): allow i18n in render --- packages/brisa/src/__fixtures__/lib/foo.tsx | 6 +- .../brisa/src/core/test/api/index.test.tsx | 72 +++++++++++++++++-- packages/brisa/src/core/test/api/index.ts | 34 ++++++++- .../test/run-web-components/index.test.ts | 7 +- .../src/core/test/run-web-components/index.ts | 5 +- packages/brisa/src/types/test.d.ts | 21 ++++-- .../testing/test-api.md | 19 +++-- 7 files changed, 141 insertions(+), 23 deletions(-) diff --git a/packages/brisa/src/__fixtures__/lib/foo.tsx b/packages/brisa/src/__fixtures__/lib/foo.tsx index 980a48b79..797f7e65d 100644 --- a/packages/brisa/src/__fixtures__/lib/foo.tsx +++ b/packages/brisa/src/__fixtures__/lib/foo.tsx @@ -1,3 +1,5 @@ -export default function Foo() { - return
Foo
; +import type { WebContext } from "brisa"; + +export default function Foo({}, { i18n }: WebContext) { + return
Foo {i18n.t("hello-world")}
; } diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index 6ce4942fc..6374bb529 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -20,6 +20,7 @@ import { } from "bun:test"; import { getConstants } from "@/constants"; import { blueLog, cyanLog, greenLog } from "@/utils/log/log-color"; +import type { RequestContext } from "brisa"; const BUILD_DIR = path.join(import.meta.dir, "..", "..", "..", "__fixtures__"); const PAGES_DIR = path.join(BUILD_DIR, "pages"); @@ -33,6 +34,7 @@ describe("test api", () => { afterEach(() => { jest.restoreAllMocks(); GlobalRegistrator.unregister(); + globalThis.mockConstants = undefined; }); describe("render", () => { it("should render the element", async () => { @@ -81,7 +83,7 @@ describe("test api", () => { return
Foo
; } const parent = document.createElement("div"); - const { container } = await render(, parent); + const { container } = await render(, { baseElement: parent }); expect(parent.contains(container)).toBeTrue(); }); @@ -151,7 +153,7 @@ describe("test api", () => { const runWebComponents = await import( "@/core/test/run-web-components" ).then((m) => m.default); - await runWebComponents(globalThis.mockConstants as any); + await runWebComponents(); // @ts-ignore const { container } = await render(); @@ -171,7 +173,7 @@ describe("test api", () => { const runWebComponents = await import( "@/core/test/run-web-components" ).then((m) => m.default); - await runWebComponents(globalThis.mockConstants as any); + await runWebComponents(); // @ts-ignore const { container } = await render(); @@ -191,7 +193,7 @@ describe("test api", () => { const runWebComponents = await import( "@/core/test/run-web-components" ).then((m) => m.default); - await runWebComponents(globalThis.mockConstants as any); + await runWebComponents(); const { container } = await render( // @ts-ignore @@ -218,7 +220,7 @@ describe("test api", () => { const runWebComponents = await import( "@/core/test/run-web-components" ).then((m) => m.default); - await runWebComponents(globalThis.mockConstants as any); + await runWebComponents(); // @ts-ignore const { container } = await render(); @@ -249,6 +251,66 @@ describe("test api", () => { "should be possible to render a server component with a web component inside", async () => {}, ); + + it("should render server component using i18n", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + I18N_CONFIG: { + defaultLocale: "en", + locales: ["en", "es"], + messages: { + en: { + "hello-world": "Hello World", + }, + }, + }, + }; + + function ServerComponent({}, { i18n }: RequestContext) { + return ( +
+ {i18n.t("hello-world")} +
+ ); + } + + // @ts-ignore + const { container } = await render(, { locale: "en" }); + const span = container.querySelector("span")!; + + expect(span.innerHTML).toBe("Hello World"); + }); + + it("should render the web component foo-component using i18n", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + I18N_CONFIG: { + defaultLocale: "en", + locales: ["en", "es"], + messages: { + en: { + "hello-world": "Hello World", + }, + }, + }, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(); + + // @ts-ignore + const { container } = await render(, { locale: "en" }); + const fooComponent = + container.querySelector("foo-component")?.shadowRoot!; + + expect(fooComponent).toContainTextContent("Foo Hello World"); + }); }); describe("cleanup", () => { diff --git a/packages/brisa/src/core/test/api/index.ts b/packages/brisa/src/core/test/api/index.ts index d9051cbd2..5135b9dd8 100644 --- a/packages/brisa/src/core/test/api/index.ts +++ b/packages/brisa/src/core/test/api/index.ts @@ -2,17 +2,47 @@ import { getServeOptions } from "@/cli/serve/serve-options"; import renderToString from "@/utils/render-to-string"; import { blueLog, greenLog, cyanLog } from "@/utils/log/log-color"; import { registerActions } from "@/utils/rpc/register-actions"; +import { getConstants } from "@/constants"; +import extendRequestContext from "@/utils/extend-request-context"; +import translateCore from "@/utils/translate-core"; /** * Render a JSX element, a string or a Response object into a container */ export async function render( element: JSX.Element | Response | string, - baseElement: HTMLElement = document.documentElement, + { + locale, + baseElement = document.documentElement, + }: { locale?: string; baseElement?: HTMLElement } = {}, ) { + const { I18N_CONFIG } = getConstants(); + const lang = locale ?? I18N_CONFIG?.defaultLocale; + let request = new Request("http://localhost"); let container = baseElement; let htmlString; + if (window.i18n && lang) { + const i18n = { + ...I18N_CONFIG, + t: translateCore(lang, I18N_CONFIG), + locale: lang, + overrideMessages(callback: any) { + const messages = I18N_CONFIG.messages?.[lang] ?? {}; + const promise = callback(messages); + const res = (extendedMessages: Record) => + Object.assign(messages, extendedMessages); + return promise.then?.(res) ?? res(promise); + }, + } as any; + + request = extendRequestContext({ + originalRequest: request, + i18n, + }); + window.i18n = i18n; + } + if (typeof element === "string") { htmlString = element; } else if (element instanceof Response) { @@ -21,7 +51,7 @@ export async function render( container = baseElement.appendChild(document.createElement("div")); globalThis.REGISTERED_ACTIONS = []; globalThis.FORCE_SUSPENSE_DEFAULT = false; - htmlString = await renderToString(element); + htmlString = await renderToString(element, { request }); globalThis.FORCE_SUSPENSE_DEFAULT = undefined; } diff --git a/packages/brisa/src/core/test/run-web-components/index.test.ts b/packages/brisa/src/core/test/run-web-components/index.test.ts index 5eea775dc..7df271d9c 100644 --- a/packages/brisa/src/core/test/run-web-components/index.test.ts +++ b/packages/brisa/src/core/test/run-web-components/index.test.ts @@ -1,5 +1,5 @@ import { expect, describe, it, beforeEach, afterEach, spyOn } from "bun:test"; -import constants, { getConstants } from "@/constants"; +import { getConstants } from "@/constants"; import { join } from "node:path"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import runWebComponents from "@/core/test/run-web-components"; @@ -48,4 +48,9 @@ describe("runWebComponents", () => { await runWebComponents(); expect(logSpy).not.toHaveBeenCalled(); }); + + it("should define i18n", async () => { + await runWebComponents(); + expect(window.i18n).toBeDefined(); + }); }); diff --git a/packages/brisa/src/core/test/run-web-components/index.ts b/packages/brisa/src/core/test/run-web-components/index.ts index bad22bd66..0beb1de47 100644 --- a/packages/brisa/src/core/test/run-web-components/index.ts +++ b/packages/brisa/src/core/test/run-web-components/index.ts @@ -4,9 +4,10 @@ import { getConstants } from "@/constants"; import { transformToWebComponents } from "@/utils/get-client-code-in-page"; import getWebComponentsList from "@/utils/get-web-components-list"; import getImportableFilepath from "@/utils/get-importable-filepath"; +import translateCore from "@/utils/translate-core"; export default async function runWebComponents() { - const { LOG_PREFIX, SRC_DIR, BUILD_DIR } = getConstants(); + const { LOG_PREFIX, SRC_DIR, BUILD_DIR, I18N_CONFIG } = getConstants(); const webComponentsDir = join(SRC_DIR, "web-components"); const internalBrisaFolder = join(BUILD_DIR, "_brisa"); const integrationsPath = getImportableFilepath( @@ -37,7 +38,7 @@ export default async function runWebComponents() { if (res) eval(res.code); if (res?.useI18n) { - // TODO: Implement i18n + window.i18n = {}; } console.log(LOG_PREFIX.READY, `transformed in ${Date.now() - time}ms`); diff --git a/packages/brisa/src/types/test.d.ts b/packages/brisa/src/types/test.d.ts index cbd28a53a..cc6fe3d4e 100644 --- a/packages/brisa/src/types/test.d.ts +++ b/packages/brisa/src/types/test.d.ts @@ -44,12 +44,21 @@ export async function render( * ``` */ element: JSX.Element | Response | string, - /** - * The base element to append the container. - * - * Default: `document.documentElement` - */ - baseElement?: HTMLElement, + options?: { + /** + * The base element to append the container. + * + * Default: `document.documentElement` + */ + baseElement?: HTMLElement; + + /** + * The i18n locale to use in the rendering. + * + * Default is the `defaultLocale`. + */ + locale?: string; + }, ): Promise<{ container: HTMLElement; unmount: () => void }>; /** diff --git a/packages/docs/building-your-application/testing/test-api.md b/packages/docs/building-your-application/testing/test-api.md index 87a7e5ece..064fe1a94 100644 --- a/packages/docs/building-your-application/testing/test-api.md +++ b/packages/docs/building-your-application/testing/test-api.md @@ -28,27 +28,36 @@ test("component", async () => { }); ``` -The second argument is an optional `baseElement` that you can use to render the component into a specific element (by default, it uses the `document.documentElement`). +The second argument are the options to render the component: + +- `baseElement`: The element where the component will be rendered. By default, it uses the `document.documentElement`. +- `locale`: The locale to use when rendering the component when using [i18n](/building-your-application/routing/internationalization). By default, it uses the `defaultLocale`. Example: ```tsx import { render } from "brisa/test"; import { test, expect } from "bun:test"; +import { Component } from "./Component"; test("component", async () => { const baseElement = document.createElement("div"); - await render(, baseElement); + await render(, { baseElement, locale: "es" }); - expect(baseElement.querySelector("button")).toHaveTextContent("Click me"); + expect(baseElement.querySelector("button")).toHaveTextContent("Clica aquí"); }); ``` Types: ```ts -render(element: JSX.Element | Response | string, baseElement?: HTMLElement -): Promise<{ container: HTMLElement, unmount: () => void }>; +render(element: JSX.Element | Response | string, options?: { + baseElement?: HTMLElement; + locale?: string; +}): Promise<{ + container: HTMLElement; + unmount: () => void; +}>; ``` ### Test server actions after rendering From 98155896e3cb64923e866342d00bf36a2b5f5486 Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 16 May 2024 00:42:28 +0200 Subject: [PATCH 10/11] test(testing): add test case of overrideMessages --- .../brisa/src/core/test/api/index.test.tsx | 70 +++++++++++++++++-- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/brisa/src/core/test/api/index.test.tsx b/packages/brisa/src/core/test/api/index.test.tsx index 6374bb529..89902e7cf 100644 --- a/packages/brisa/src/core/test/api/index.test.tsx +++ b/packages/brisa/src/core/test/api/index.test.tsx @@ -247,10 +247,37 @@ describe("test api", () => { expect(customCounter).toContainTextContent("0"); }); - it.todo( - "should be possible to render a server component with a web component inside", - async () => {}, - ); + it("should be possible to render a server component with a web component inside", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + }; + // Register DOM and web components from __fixtures__/web-components + const runWebComponents = await import( + "@/core/test/run-web-components" + ).then((m) => m.default); + await runWebComponents(); + + function ServerComponent() { + return ( +
+ {/* @ts-ignore */} + +
+ ); + } + + // @ts-ignore + const { container } = await render(); + const customCounter = + container.querySelector("custom-counter")?.shadowRoot!; + const [increment] = customCounter.querySelectorAll("button"); + + expect(customCounter).toContainTextContent("0"); + userEvent.click(increment); + expect(customCounter).toContainTextContent("1"); + }); it("should render server component using i18n", async () => { globalThis.mockConstants = { @@ -283,6 +310,41 @@ describe("test api", () => { expect(span.innerHTML).toBe("Hello World"); }); + it("should be possible to use overrideMessages inside a server component", async () => { + globalThis.mockConstants = { + ...(getConstants() ?? {}), + SRC_DIR: BUILD_DIR, + BUILD_DIR, + I18N_CONFIG: { + defaultLocale: "en", + locales: ["en", "es"], + messages: { + en: { + "hello-world": "Hello World", + }, + }, + }, + }; + + function ServerComponent({}, { i18n }: RequestContext) { + i18n.overrideMessages(() => ({ + hello: "Hi {{name}}", + })); + + return ( +
+ {i18n.t("hello", { name: "Foo" })} +
+ ); + } + + // @ts-ignore + const { container } = await render(, { locale: "en" }); + const span = container.querySelector("span")!; + + expect(span.innerHTML).toBe("Hi Foo"); + }); + it("should render the web component foo-component using i18n", async () => { globalThis.mockConstants = { ...(getConstants() ?? {}), From acfed78fc4607a53c57df0b2982ab7c19f8e54fc Mon Sep 17 00:00:00 2001 From: Aral Roca Date: Thu, 16 May 2024 00:47:57 +0200 Subject: [PATCH 11/11] docs(testing): add how to test web components --- .../testing/test-api.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/docs/building-your-application/testing/test-api.md b/packages/docs/building-your-application/testing/test-api.md index 064fe1a94..4d3948d8b 100644 --- a/packages/docs/building-your-application/testing/test-api.md +++ b/packages/docs/building-your-application/testing/test-api.md @@ -107,6 +107,35 @@ test("component", async () => { > > [`rerenderInAction`](/api-reference/server-apis/rerenderInAction), [`navigate`](/api-reference/functions/navigate), and other actions that change the state of the application are not available in the test environment. You can use the [`mock`](https://bun.sh/docs/test/mocks) function to simulate the server action and test the component behavior. +### Test Web Components after rendering + +You can also test Web Components after rendering them. For example, you can test a custom element: + +```tsx +import { render, userEvent } from "brisa/test"; +import { test, expect } from "bun:test"; + +test("web component", async () => { + const { container } = await render(); + const counter = container.querySelector("custom-counter")!.shadowRoot!; + const [increment, decrement] = counter.querySelectorAll("button"); + + expect(counter).toContainTextContent("0"); + + userEvent.click(increment); + + expect(counter).toContainTextContent("1"); + + userEvent.click(decrement); + + expect(counter).toContainTextContent("0"); +}); +``` + +> [!TIP] +> +> You can use the `shadowRoot` property to access the shadow DOM of a custom element. + ## `serveRoute` Request a Brisa route and return the [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). These routes can be API endpoints, pages, assets, or any other type of route.