Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose normalize api #24

Merged
merged 1 commit into from
Feb 14, 2024
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,21 @@ With mocha you can use `mocha -r @marko/testing-library/dont-cleanup-after-each`

If you are using Jest, you can include `setupFilesAfterEnv: ["@marko/testing-library/dont-cleanup-after-each"]` in your Jest config to avoid doing this in each file.

### `normalize()`

Returns a clone of the passed DOM container with Marko's internal markers removed (data-marko, etc.), id's and whitespace are also normalized.

```javascript
import { render, normalize } from "@marko/testing-library";
import HelloTemplate from "./src/__test__/fixtures/hello-name.marko";

test("snapshot", async () => {
const { container } = await render(HelloTemplate, { name: "World" });

expect(normalize(container)).toMatchSnapshot();
});
```

## Setup

Marko testing library is not dependent on any test runner, however it is dependent on the test environment. These utilities work for testing both server side, and client side Marko templates and provide a slightly different implementation for each. This is done using a [browser shim](https://github.com/defunctzombie/package-browser-field-spec), just like in Marko.
Expand Down
26 changes: 25 additions & 1 deletion src/__tests__/render.browser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { render, fireEvent, screen, cleanup, act } from "../index-browser";
import {
render,
fireEvent,
screen,
cleanup,
act,
normalize,
} from "../index-browser";
import Counter from "./fixtures/counter.marko";
import SplitCounter from "./fixtures/split-counter.marko";
import LegacyCounter from "./fixtures/legacy-counter";
Expand All @@ -18,6 +25,23 @@ test("renders interactive content in the document", async () => {
expect(getByText("Value: 1")).toBeInTheDocument();
});

test("normalizes a rendered containers content", async () => {
const { container } = await render(Counter);

expect(normalize(container)).toMatchInlineSnapshot(`
<div>
<div
class="counter"
>
Value: 0
<button>
Increment
</button>
</div>
</div>
`);
});

test("renders interactive split component in the document", async () => {
const { getByText } = await render(SplitCounter, { message: "Count" });
expect(getByText(/0/)).toBeInTheDocument();
Expand Down
18 changes: 17 additions & 1 deletion src/__tests__/render.server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render, screen, fireEvent, cleanup, act } from "..";
import { render, screen, fireEvent, cleanup, act, normalize } from "..";
import Counter from "./fixtures/counter.marko";
import SplitCounter from "./fixtures/split-counter.marko";
import LegacyCounter from "./fixtures/legacy-counter";
Expand All @@ -17,6 +17,22 @@ test("renders static content in a document with a browser context", async () =>
expect(container.firstElementChild).toHaveAttribute("class", "counter");
});

test("normalizes a rendered containers content", async () => {
const { container } = await render(Counter);
expect(normalize(container)).toMatchInlineSnapshot(`
<DocumentFragment>
<div
class="counter"
>
Value: 0
<button>
Increment
</button>
</div>
</DocumentFragment>
`);
});

test("renders split component in the document", async () => {
const { getByText } = await render(SplitCounter, { message: "Count" });
expect(getByText(/0/)).toBeDefined();
Expand Down
2 changes: 1 addition & 1 deletion src/index-browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface MountedComponent {
}
const mountedComponents = new Set<MountedComponent>();

export { FireFunction, FireObject, fireEvent, act } from "./shared";
export { FireFunction, FireObject, fireEvent, act, normalize } from "./shared";

export type RenderResult = AsyncReturnValue<typeof render>;

Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import {
screen as testingLibraryScreen,
} from "@testing-library/dom";

export { FireFunction, FireObject, fireEvent, act } from "./shared";
export { FireFunction, FireObject, fireEvent, act, normalize } from "./shared";

export type RenderResult = AsyncReturnValue<typeof render>;

export const screen: typeof testingLibraryScreen = {} as any;

let activeContainer: DocumentFragment | undefined;

export async function render<T extends Marko.Template>(
export async function render<T extends Marko.Template<any, any>>(
template: T | { default: T },
input: Marko.TemplateInput<Marko.Input<T>> = {} as any,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
81 changes: 81 additions & 0 deletions src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,83 @@ export type FireObject = {
) => Promise<ReturnType<originalFireFunction>>;
};

const SHOW_ELEMENT = 1;
const SHOW_COMMENT = 128;
const COMMENT_NODE = 8;

export function normalize<T extends DocumentFragment | Element>(container: T) {
const idMap: Map<string, number> = new Map();
const clone = container.cloneNode(true) as T;
const document = container.ownerDocument!;
const commentAndElementWalker = document.createTreeWalker(
clone,
SHOW_ELEMENT | SHOW_COMMENT
);

let node: Comment | Element;
let nextNode = commentAndElementWalker.nextNode();
while ((node = nextNode as Comment | Element)) {
nextNode = commentAndElementWalker.nextNode();
if (isComment(node)) {
node.remove();
} else {
const { id, attributes } = node;
if (/\d/.test(id)) {
let idIndex = idMap.get(id);

if (idIndex === undefined) {
idIndex = idMap.size;
idMap.set(id, idIndex);
}

node.id = `GENERATED-${idIndex}`;
}

for (let i = attributes.length; i--; ) {
const attr = attributes[i];

if (/^data-(w-|widget$|marko(-|$))/.test(attr.name)) {
node.removeAttributeNode(attr);
}
}
}
}

if (idMap.size) {
const elementWalker = document.createTreeWalker(clone, SHOW_ELEMENT);

nextNode = elementWalker.nextNode();
while ((node = nextNode as Element)) {
nextNode = elementWalker.nextNode();
const { attributes } = node;

for (let i = attributes.length; i--; ) {
const attr = attributes[i];
const { value } = attr;
const updated = value
.split(" ")
.map((part) => {
const idIndex = idMap.get(part);
if (idIndex === undefined) {
return part;
}

return `GENERATED-${idIndex}`;
})
.join(" ");

if (value !== updated) {
attr.value = updated;
}
}
}
}

clone.normalize();

return clone;
}

export async function act<T extends (...args: unknown[]) => unknown>(fn: T) {
type Return = ReturnType<T>;
if (typeof window === "undefined") {
Expand Down Expand Up @@ -94,3 +171,7 @@ const tick =
function waitForBatchedUpdates() {
return new Promise(tick);
}

function isComment(node: Node): node is Comment {
return node.nodeType === COMMENT_NODE;
}
2 changes: 1 addition & 1 deletion src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ declare namespace Marko {
// against the v3 compat layer in Marko 4.

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Template {}
export interface Template<Input = unknown, Return = unknown> {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export type TemplateInput<T> = any;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
Loading