Skip to content

Commit

Permalink
feat: expose normalize api (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey authored Feb 14, 2024
1 parent 3411600 commit 80bd74a
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 6 deletions.
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

0 comments on commit 80bd74a

Please sign in to comment.