Skip to content
Open
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@
"visio",
"Vite",
"Vitest",
"vnode",
"vuejs",
"WCAG",
"ytlikecount",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
applyTo: 'packages/atomic/src/components/**/stencil-*.tsx, packages/atomic/src/components/**/stencil-*.spec.ts'
---

## Scope
- Use these instructions when writing unit tests for Stencil `FunctionalComponent` exports in `packages/atomic`.
- Keep assertions framework-agnostic so that future implementation changes require minimal test updates.

## Required References
- Follow [Atomic component instructions](./atomic.instructions.md) for package conventions.
- Follow [Atomic test instructions](./tests-atomic.instructions.md) for structure, naming, mocking patterns, and running tests.
- Review existing examples such as `packages/atomic/src/components/search/result-template-components/quickview-iframe/stencil-quickview-iframe.spec.ts` before creating new tests.

## Production files are read-only.

Do NOT edit the production files (.tsx)

If you can't create unit tests without modifying the production files, stop and ask the user for help.

## Identify Files to Test
- Components reside under `packages/atomic/src/components/<feature>/<name>/`.
- Each folder contains one primary file named `stencil-<component>.tsx` matching the folder name.
- Additional `.tsx` files in the same folder that export a Stencil `FunctionalComponent` must also receive unit tests.

## Test File Setup
- Place new spec files beside the implementation and name them `stencil-<component>.spec.ts`.
- Test files must use `.ts` (no JSX syntax).
- Import utilities from `@stencil/core` using `type`-only imports to avoid bundling runtime code.
- Use `renderStencilVNode` from `@/vitest-utils/testing-helpers/stencil-vnode-renderer` to mount the virtual node into the DOM prior to assertions.
- Avoid importing `Fragment` from `@stencil/core`; prefer arrays or wrapper elements when a fragment-like structure is required.

### Boilerplate Pattern
```ts
import type {VNode} from '@stencil/core';
import {
afterEach,
beforeEach,
describe,
expect,
it,
vi,
} from 'vitest';
import {renderStencilVNode} from '@/vitest-utils/testing-helpers/stencil-vnode-renderer';
import {Component} from './stencil-component';

describe('Component (Stencil)', () => {
let container: HTMLElement;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
});

afterEach(() => {
container.remove();
});

const renderComponent = async (props?: {
/* component-specific props */
}) => {
const vnode = Component(
{
/* component-specific props */
},
[],
// biome-ignore lint/suspicious/noExplicitAny: Stencil FunctionalComponent utils parameter is unused in tests.
{} as any
) as VNode;

await renderStencilVNode(vnode, container);

const element = container.firstElementChild;

return {element};
};

it('should ...', async () => {
const {element} = await renderComponent({/* props */});
expect(element).toBeTruthy();
});
});
```

### Internationalization Helpers
- When a component relies on i18n, use `createTestI18n` from `@/vitest-utils/testing-helpers/i18n-utils`.

```ts
import type {i18n} from 'i18next';
import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';

describe('Component (Stencil)', () => {
let i18n: i18n;

beforeAll(async () => {
i18n = await createTestI18n();
});
});
```

## Test Guidelines
- Cover every exported `FunctionalComponent` in the folder.
- Focus on observable DOM output and events rather than internal implementation details.
- Prefer helper functions within the spec for repeated setup or assertions.
- Do not edit Stencil production `.tsx` files while authoring tests. If changes seem necessary, pause and escalate.
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
agent: agent
description: 'Create unit tests for each Stencil FunctionalComponent in the component folder'
---

Follow the [`tests-atomic-stencil-functional-component`](../instructions/tests-atomic-stencil-functional-component.instructions.md) instructions

The unit tests should be as generic as possible such that the future migration to Lit creates the minimal diff.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {LinkWithItemAnalytics} from '../../../common/item-link/stencil-item-link
import {Button} from '../../../common/stencil-button';
import {Bindings} from '../../atomic-search-interface/atomic-search-interface';
import {QuickviewSidebar} from '../atomic-quickview-sidebar/atomic-quickview-sidebar';
import {QuickviewIframe} from '../quickview-iframe/quickview-iframe';
import {QuickviewIframe} from '../quickview-iframe/stencil-quickview-iframe';
import {buildQuickviewPreviewBar} from '../quickview-preview-bar/quickview-preview-bar';
import {
getWordsHighlights,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type {SearchEngine} from '@coveo/headless';
import type {VNode} from '@stencil/core';
import {html} from 'lit';
import {beforeEach, describe, expect, it, vi} from 'vitest';
import {fixtureWrapper} from '@/vitest-utils/testing-helpers/fixture-wrapper';
import {renderStencilVNode} from '@/vitest-utils/testing-helpers/stencil-vnode-renderer';
import {QuickviewIframe} from './quickview-iframe';
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
import {renderQuickviewIframe} from './quickview-iframe';

describe('QuickviewIframe (Stencil)', () => {
describe('#renderQuickviewIframe', () => {
let mockOnSetIframeRef: (ref: HTMLIFrameElement) => void;
let mockLogger: SearchEngine['logger'];

Expand All @@ -16,13 +15,6 @@ describe('QuickviewIframe (Stencil)', () => {
} as unknown as SearchEngine['logger'];
});

/**
* Helper function to render the QuickviewIframe component.
* This calls the actual Stencil functional component and renders its output.
*
* For Lit migration: Replace this with a helper that uses renderFunctionFixture
* with the Lit component.
*/
const renderComponent = async (props: {
title: string;
content?: string;
Expand All @@ -32,18 +24,14 @@ describe('QuickviewIframe (Stencil)', () => {
src?: string;
logger?: SearchEngine['logger'];
}): Promise<HTMLIFrameElement> => {
// Call the actual Stencil functional component with required parameters
const vnode = QuickviewIframe(
props,
[], // children
// biome-ignore lint/suspicious/noExplicitAny: Stencil FunctionalComponent requires utils parameter but it's not used
{} as any
) as VNode;

const container = document.createElement('div');
fixtureWrapper(container);

await renderStencilVNode(vnode, container);
const container = await renderFunctionFixture(
html`${renderQuickviewIframe({props})}`
);

// Twice because renderQuickviewIframe has 2 nested async updates
await new Promise((resolve) => setTimeout(resolve));
await new Promise((resolve) => setTimeout(resolve));

return container.firstElementChild as HTMLIFrameElement;
};

Expand Down Expand Up @@ -240,6 +228,58 @@ describe('QuickviewIframe (Stencil)', () => {
});
});

describe('when contentDocument is unavailable', () => {
it('should log a warning and set fallback src when provided', async () => {
const contentDocumentSpy = vi
.spyOn(HTMLIFrameElement.prototype, 'contentDocument', 'get')
.mockReturnValue(null as unknown as Document);

try {
const fallbackSrc = 'https://example.com/quickview';

const iframe = await renderComponent({
title: 'Test Title',
content: '<p>Content</p>',
onSetIframeRef: mockOnSetIframeRef,
uniqueIdentifier: 'test-id',
logger: mockLogger,
src: fallbackSrc,
});

expect(mockLogger.warn).toHaveBeenCalledTimes(1);
expect(mockLogger.warn).toHaveBeenCalledWith(
'Quickview initialized in restricted mode due to incompatible sandboxing environment. Keywords hit navigation will be disabled.'
);
expect(iframe.getAttribute('src')).toBe(fallbackSrc);
expect(mockOnSetIframeRef).not.toHaveBeenCalled();
} finally {
contentDocumentSpy.mockRestore();
}
});

it('should return early without logging when fallback src is not provided', async () => {
const contentDocumentSpy = vi
.spyOn(HTMLIFrameElement.prototype, 'contentDocument', 'get')
.mockReturnValue(null as unknown as Document);

try {
const iframe = await renderComponent({
title: 'Test Title',
content: '<p>Content</p>',
onSetIframeRef: mockOnSetIframeRef,
uniqueIdentifier: 'test-id',
logger: mockLogger,
});

expect(mockLogger.warn).not.toHaveBeenCalled();
expect(iframe.getAttribute('src')).toBe('about:blank');
expect(mockOnSetIframeRef).not.toHaveBeenCalled();
} finally {
contentDocumentSpy.mockRestore();
}
});
});

describe('async behavior', () => {
it('should call onSetIframeRef after content is written asynchronously', async () => {
const callOrder: string[] = [];
Expand All @@ -263,28 +303,25 @@ describe('QuickviewIframe (Stencil)', () => {
});

describe('edge cases', () => {
it('should handle empty content string', async () => {
const iframe = await renderComponent({
it('should not call onSetIframeRef when content is falsy', async () => {
await renderComponent({
title: 'Test Title',
content: '',
onSetIframeRef: mockOnSetIframeRef,
uniqueIdentifier: 'test-id',
});

const iframeDoc = iframe.contentDocument;
// Empty string is falsy, so onSetIframeRef should not be called
expect(iframeDoc?.body.innerHTML).not.toContain('CoveoDocIdentifier');
expect(mockOnSetIframeRef).not.toHaveBeenCalled();
});

it('should handle empty uniqueIdentifier string', async () => {
it('should not call onSetIframeRef when uniqueIdentifier is falsy', async () => {
await renderComponent({
title: 'Test Title',
content: '<p>Content</p>',
onSetIframeRef: mockOnSetIframeRef,
uniqueIdentifier: '',
});

// Empty string is falsy, so onSetIframeRef should not be called
expect(mockOnSetIframeRef).not.toHaveBeenCalled();
});

Expand Down
Loading
Loading