Skip to content

Commit 8836a50

Browse files
authored
feat: add ability to capture DOM snapshots of specific elements (#1098)
* feat: add ability to capture DOM snapshots of specific elements * fix: fix review issues regarding undefined usage
1 parent 3a1b1da commit 8836a50

3 files changed

Lines changed: 187 additions & 21 deletions

File tree

src/browser/commands/captureDomSnapshot.ts

Lines changed: 81 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,37 @@ export interface CaptureSnapshotResult {
1616
textWasTruncated: boolean;
1717
}
1818

19-
export const captureDomSnapshotInBrowser = ({
20-
includeTags = [],
21-
includeAttrs = [],
22-
excludeTags = [],
23-
excludeAttrs = [],
24-
truncateText = true,
25-
maxTextLength = 100,
26-
}: CaptureSnapshotOptions = {}): CaptureSnapshotResult => {
19+
export const captureDomSnapshotInBrowser = (
20+
selectorOrElementOrOptions?: string | WebdriverIO.Element | CaptureSnapshotOptions,
21+
maybeOptions?: CaptureSnapshotOptions,
22+
): CaptureSnapshotResult => {
23+
let selector: string | null = null;
24+
let element: Element | null = null;
25+
let options: CaptureSnapshotOptions;
26+
27+
if (typeof selectorOrElementOrOptions === "string") {
28+
selector = selectorOrElementOrOptions;
29+
options = maybeOptions || {};
30+
} else if (
31+
selectorOrElementOrOptions &&
32+
typeof selectorOrElementOrOptions === "object" &&
33+
"tagName" in selectorOrElementOrOptions
34+
) {
35+
element = selectorOrElementOrOptions as Element;
36+
options = maybeOptions || {};
37+
} else {
38+
options = (selectorOrElementOrOptions as CaptureSnapshotOptions) || {};
39+
}
40+
41+
const {
42+
includeTags = [],
43+
includeAttrs = [],
44+
excludeTags = [],
45+
excludeAttrs = [],
46+
truncateText = true,
47+
maxTextLength = 100,
48+
} = options;
49+
2750
const BASE_EXCLUDED_TAGS = new Set([
2851
"HEAD",
2952
"LINK",
@@ -403,14 +426,39 @@ export const captureDomSnapshotInBrowser = ({
403426
}
404427
}
405428

406-
const startElement = document.body || document.documentElement;
407-
if (!startElement) {
408-
return {
409-
snapshot: "# No elements found",
410-
omittedTags: [],
411-
omittedAttributes: [],
412-
textWasTruncated: false,
413-
};
429+
let startElement: Element | null;
430+
431+
if (element) {
432+
startElement = element;
433+
} else if (selector) {
434+
try {
435+
startElement = document.querySelector(selector);
436+
if (!startElement) {
437+
return {
438+
snapshot: `# Element not found: ${selector}`,
439+
omittedTags: [],
440+
omittedAttributes: [],
441+
textWasTruncated: false,
442+
};
443+
}
444+
} catch (error) {
445+
return {
446+
snapshot: `# Invalid selector: ${selector}`,
447+
omittedTags: [],
448+
omittedAttributes: [],
449+
textWasTruncated: false,
450+
};
451+
}
452+
} else {
453+
startElement = document.body || document.documentElement;
454+
if (!startElement) {
455+
return {
456+
snapshot: "# No elements found",
457+
omittedTags: [],
458+
omittedAttributes: [],
459+
textWasTruncated: false,
460+
};
461+
}
414462
}
415463

416464
const rootNode = buildElementNode(startElement);
@@ -430,8 +478,23 @@ export default (browser: Browser): void => {
430478

431479
session.addCommand(
432480
"unstable_captureDomSnapshot",
433-
async function (options: Partial<CaptureSnapshotOptions> = {}): Promise<CaptureSnapshotResult> {
434-
return session.execute(captureDomSnapshotInBrowser, options);
481+
async function (
482+
this: WebdriverIO.Browser,
483+
selectorOrOptions?: string | Partial<CaptureSnapshotOptions>,
484+
maybeOptions?: Partial<CaptureSnapshotOptions>,
485+
): Promise<CaptureSnapshotResult> {
486+
return session.execute(captureDomSnapshotInBrowser, selectorOrOptions, maybeOptions);
487+
},
488+
);
489+
490+
session.addCommand(
491+
"unstable_captureDomSnapshot",
492+
async function (
493+
this: WebdriverIO.Element,
494+
options: Partial<CaptureSnapshotOptions> = {},
495+
): Promise<CaptureSnapshotResult> {
496+
return this.execute(captureDomSnapshotInBrowser, options);
435497
},
498+
true,
436499
);
437500
};

src/browser/types.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,15 @@ declare global {
153153

154154
clearSession: (this: WebdriverIO.Browser) => Promise<void>;
155155

156-
unstable_captureDomSnapshot: (
156+
unstable_captureDomSnapshot(
157157
this: WebdriverIO.Browser,
158-
options: CaptureSnapshotOptions,
159-
) => Promise<CaptureSnapshotResult>;
158+
options?: Partial<CaptureSnapshotOptions>,
159+
): Promise<CaptureSnapshotResult>;
160+
unstable_captureDomSnapshot(
161+
this: WebdriverIO.Browser,
162+
selector: string,
163+
options?: Partial<CaptureSnapshotOptions>,
164+
): Promise<CaptureSnapshotResult>;
160165
}
161166

162167
interface Element {
@@ -193,6 +198,11 @@ declare global {
193198
assertView: AssertViewElementCommand;
194199

195200
moveCursorTo: MoveCursorToCommand;
201+
202+
unstable_captureDomSnapshot(
203+
this: WebdriverIO.Element,
204+
options?: Partial<CaptureSnapshotOptions>,
205+
): Promise<CaptureSnapshotResult>;
196206
}
197207
}
198208
}

test/src/browser/commands/captureDomSnapshot.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,4 +689,97 @@ describe('"captureDomSnapshot" command', () => {
689689
assert.deepEqual(result.snapshot.split("\n"), expected.split("\n"));
690690
});
691691
});
692+
693+
describe("selector-based capture", () => {
694+
beforeEach(() => {
695+
document.body.innerHTML = `
696+
<div class="container">
697+
<h1>Header</h1>
698+
<div class="content">
699+
<p>Paragraph content</p>
700+
<button id="test-button">Click me</button>
701+
</div>
702+
<footer class="footer">Footer content</footer>
703+
</div>
704+
`;
705+
});
706+
707+
it("should capture snapshot of specific element by selector", () => {
708+
const result = captureDomSnapshotInBrowser(".content");
709+
710+
assert.match(result.snapshot, /div\.content.*@hidden.*:/);
711+
assert.match(result.snapshot, /p.*@hidden.*"Paragraph content"/);
712+
assert.match(result.snapshot, /button#test-button.*@hidden.*"Click me"/);
713+
assert.notMatch(result.snapshot, /h1.*"Header"/);
714+
assert.notMatch(result.snapshot, /footer.*"Footer content"/);
715+
});
716+
717+
it("should return error message for non-existent selector", () => {
718+
const result = captureDomSnapshotInBrowser(".non-existent");
719+
720+
assert.equal(result.snapshot, "# Element not found: .non-existent");
721+
assert.deepEqual(result.omittedTags, []);
722+
assert.deepEqual(result.omittedAttributes, []);
723+
assert.equal(result.textWasTruncated, false);
724+
});
725+
726+
it("should return error message for invalid selector", () => {
727+
const result = captureDomSnapshotInBrowser("[invalid selector");
728+
729+
assert.equal(result.snapshot, "# Invalid selector: [invalid selector");
730+
assert.deepEqual(result.omittedTags, []);
731+
assert.deepEqual(result.omittedAttributes, []);
732+
assert.equal(result.textWasTruncated, false);
733+
});
734+
735+
it("should support options with selector", () => {
736+
const result = captureDomSnapshotInBrowser(".content", {
737+
truncateText: false,
738+
maxTextLength: 5,
739+
});
740+
741+
assert.match(result.snapshot, /div\.content.*@hidden.*:/);
742+
assert.match(result.snapshot, /p.*@hidden.*"Paragraph content"/);
743+
assert.equal(result.textWasTruncated, false);
744+
});
745+
});
746+
747+
describe("element-based capture", () => {
748+
let testElement: Element;
749+
750+
beforeEach(() => {
751+
document.body.innerHTML = `
752+
<div class="container">
753+
<h1>Header</h1>
754+
<div class="content" id="target-content">
755+
<p>Paragraph content</p>
756+
<button id="test-button">Click me</button>
757+
</div>
758+
<footer class="footer">Footer content</footer>
759+
</div>
760+
`;
761+
testElement = document.querySelector(".content") as Element;
762+
});
763+
764+
it("should capture snapshot of specific element passed directly", () => {
765+
const result = captureDomSnapshotInBrowser(testElement as any);
766+
767+
assert.match(result.snapshot, /div\.content#target-content.*@hidden.*:/);
768+
assert.match(result.snapshot, /p.*@hidden.*"Paragraph content"/);
769+
assert.match(result.snapshot, /button#test-button.*@hidden.*"Click me"/);
770+
assert.notMatch(result.snapshot, /h1.*"Header"/);
771+
assert.notMatch(result.snapshot, /footer.*"Footer content"/);
772+
});
773+
774+
it("should support options with element", () => {
775+
const result = captureDomSnapshotInBrowser(testElement as any, {
776+
maxTextLength: 5,
777+
truncateText: true,
778+
});
779+
780+
assert.match(result.snapshot, /div\.content#target-content.*@hidden.*:/);
781+
assert.match(result.snapshot, /p.*@hidden.*"Parag\.\.\."/);
782+
assert.equal(result.textWasTruncated, true);
783+
});
784+
});
692785
});

0 commit comments

Comments
 (0)