1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-08 20:50:28 +00:00

Adds content performance test.

This commit is contained in:
Miles Blackwood
2026-02-03 16:19:01 -05:00
parent c1c89d3917
commit 42b637260c

View File

@@ -0,0 +1,274 @@
import AutofillField from "../models/autofill-field";
import { CollectAutofillContentService } from "./collect-autofill-content.service";
import DomElementVisibilityService from "./dom-element-visibility.service";
import { DomQueryService } from "./dom-query.service";
describe("CollectAutofillContentService - Performance Tests", () => {
let service: CollectAutofillContentService;
let domQueryService: DomQueryService;
let domElementVisibilityService: DomElementVisibilityService;
beforeEach(() => {
domElementVisibilityService = new DomElementVisibilityService();
domQueryService = new DomQueryService();
service = new CollectAutofillContentService(domElementVisibilityService, domQueryService);
});
describe("getAutofillFieldElementByOpid performance", () => {
it("should demonstrate O(1) lookup performance with dual-index cache", () => {
// Create a large number of mock form fields
const fieldCount = 1000;
const mockFields: Array<{
element: HTMLInputElement & { opid: string };
data: any;
}> = [];
// Append container to document so elements are connected
const container = document.createElement("div");
document.body.appendChild(container);
// Create mock fields and cache them
for (let i = 0; i < fieldCount; i++) {
const input = document.createElement("input") as HTMLInputElement & { opid: string };
input.type = "text";
input.name = `field_${i}`;
input.id = `field_${i}`;
input.opid = `__${i}`;
container.appendChild(input); // Add to DOM so isConnected = true
const autofillFieldData = {
opid: `__${i}`,
elementNumber: i,
htmlID: `field_${i}`,
htmlName: `field_${i}`,
type: "text",
viewable: true,
} as AutofillField;
mockFields.push({ element: input, data: autofillFieldData });
// Cache the element using the private method
service["cacheAutofillFieldElement"](i, input, autofillFieldData);
}
// Verify cache is populated
expect(service["autofillFieldElements"].size).toBe(fieldCount);
expect(service["autofillFieldsByOpid"].size).toBe(fieldCount);
// Perform multiple lookups and measure performance
const lookupsCount = 100;
const opidsToLookup: string[] = [];
// Generate random opids to lookup
for (let i = 0; i < lookupsCount; i++) {
const randomIndex = Math.floor(Math.random() * fieldCount);
opidsToLookup.push(`__${randomIndex}`);
}
const startTime = performance.now();
let successfulLookups = 0;
for (const opid of opidsToLookup) {
const result = service.getAutofillFieldElementByOpid(opid);
if (result) {
successfulLookups++;
}
}
const endTime = performance.now();
const totalTime = endTime - startTime;
const avgTimePerLookup = totalTime / lookupsCount;
// All lookups should succeed
expect(successfulLookups).toBe(lookupsCount);
// Performance assertions
// With 1000 fields and O(1) lookup, average time should be < 0.1ms per lookup
// Old O(n) approach would be much slower (typically > 1ms per lookup with 1000 fields)
expect(avgTimePerLookup).toBeLessThan(0.1);
// Total time for 100 lookups should be < 10ms with O(1) approach
expect(totalTime).toBeLessThan(10);
});
it("should handle stale element cleanup correctly", () => {
const container = document.createElement("div");
document.body.appendChild(container);
const input = document.createElement("input") as HTMLInputElement & { opid: string };
input.type = "text";
input.opid = "__test";
container.appendChild(input);
const autofillFieldData = {
opid: "__test",
elementNumber: 0,
htmlID: "test",
type: "text",
viewable: true,
} as AutofillField;
// Cache the element
service["cacheAutofillFieldElement"](0, input, autofillFieldData);
// Verify it's cached in both maps
expect(service["autofillFieldElements"].size).toBe(1);
expect(service["autofillFieldsByOpid"].size).toBe(1);
// First lookup should succeed
const result = service.getAutofillFieldElementByOpid("__test");
expect(result).toBe(input);
// Simulate element being removed from DOM
container.removeChild(input);
// Verify the cached element is no longer connected
expect(input.isConnected).toBe(false);
// Manually clean up stale entry (simulating what getAutofillFieldElementByOpid does)
service["autofillFieldElements"].delete(input);
service["autofillFieldsByOpid"].delete("__test");
// Verify both maps are cleaned up
expect(service["autofillFieldElements"].size).toBe(0);
expect(service["autofillFieldsByOpid"].size).toBe(0);
});
it("should handle opid replacement correctly when same opid is reused", () => {
const container = document.createElement("div");
document.body.appendChild(container);
// Create first element with opid "__1"
const input1 = document.createElement("input") as HTMLInputElement & { opid: string };
input1.type = "text";
input1.id = "field1";
input1.opid = "__1";
container.appendChild(input1);
const data1 = {
opid: "__1",
elementNumber: 0,
htmlID: "field1",
type: "text",
viewable: true,
} as AutofillField;
service["cacheAutofillFieldElement"](0, input1, data1);
// Verify first element is cached
expect(service.getAutofillFieldElementByOpid("__1")).toBe(input1);
expect(service["autofillFieldElements"].size).toBe(1);
// Create second element with same opid "__1" (simulating dynamic content)
const input2 = document.createElement("input") as HTMLInputElement & { opid: string };
input2.type = "text";
input2.id = "field2";
input2.opid = "__1";
container.appendChild(input2);
const data2 = {
opid: "__1",
elementNumber: 1,
htmlID: "field2",
type: "text",
viewable: true,
} as AutofillField;
service["cacheAutofillFieldElement"](1, input2, data2);
// Verify second element replaced first in opid map
expect(service.getAutofillFieldElementByOpid("__1")).toBe(input2);
// Old element should be removed from autofillFieldElements
expect(service["autofillFieldElements"].has(input1)).toBe(false);
expect(service["autofillFieldElements"].has(input2)).toBe(true);
// Only one entry in autofillFieldsByOpid map
expect(service["autofillFieldsByOpid"].size).toBe(1);
expect(service["autofillFieldsByOpid"].get("__1")).toBe(input2);
});
it("should clear both maps when clearCache is called", () => {
// Cache some elements
for (let i = 0; i < 10; i++) {
const input = document.createElement("input") as HTMLInputElement & { opid: string };
input.opid = `__${i}`;
service["cacheAutofillFieldElement"](i, input, {
opid: `__${i}`,
elementNumber: i,
type: "text",
viewable: true,
} as AutofillField);
}
expect(service["autofillFieldElements"].size).toBe(10);
expect(service["autofillFieldsByOpid"].size).toBe(10);
// Simulate navigation (which clears cache)
// Access private method for testing
service["_autofillFormElements"].clear();
service["autofillFieldElements"].clear();
service["autofillFieldsByOpid"].clear();
expect(service["autofillFieldElements"].size).toBe(0);
expect(service["autofillFieldsByOpid"].size).toBe(0);
});
});
describe("Shadow DOM field caching", () => {
it("should cache and retrieve shadow DOM fields with same performance", () => {
// Create shadow DOM container and add to document
const shadowHost = document.createElement("div");
document.body.appendChild(shadowHost);
const shadowRoot = shadowHost.attachShadow({ mode: "open" });
// Create fields in shadow DOM
const shadowInput1 = document.createElement("input") as HTMLInputElement & {
opid: string;
};
shadowInput1.type = "password";
shadowInput1.name = "password";
shadowInput1.opid = "__shadow_1";
shadowRoot.appendChild(shadowInput1);
const shadowInput2 = document.createElement("input") as HTMLInputElement & {
opid: string;
};
shadowInput2.type = "email";
shadowInput2.name = "email";
shadowInput2.opid = "__shadow_2";
shadowRoot.appendChild(shadowInput2);
// Cache shadow DOM fields
service["cacheAutofillFieldElement"](0, shadowInput1, {
opid: "__shadow_1",
elementNumber: 0,
type: "password",
viewable: true,
} as AutofillField);
service["cacheAutofillFieldElement"](1, shadowInput2, {
opid: "__shadow_2",
elementNumber: 1,
type: "email",
viewable: true,
} as AutofillField);
// Verify retrieval works
expect(service.getAutofillFieldElementByOpid("__shadow_1")).toBe(shadowInput1);
expect(service.getAutofillFieldElementByOpid("__shadow_2")).toBe(shadowInput2);
// Performance should be same O(1) regardless of DOM tree structure
const startTime = performance.now();
for (let i = 0; i < 100; i++) {
service.getAutofillFieldElementByOpid("__shadow_1");
}
const endTime = performance.now();
// Should still be fast even with shadow DOM
expect(endTime - startTime).toBeLessThan(10);
});
});
});