Skip to content
Open
10 changes: 10 additions & 0 deletions src/test/setup-webview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ if (
};
}

// Polyfill for requestAnimationFrame / cancelAnimationFrame (not implemented by JSDOM)
if (!global.requestAnimationFrame) {
global.requestAnimationFrame = (cb: FrameRequestCallback): number => {
return setTimeout(() => cb(Date.now()), 0) as unknown as number;
};
global.cancelAnimationFrame = (id: number): void => {
clearTimeout(id);
};
}

// Polyfill for ResizeObserver (required by @vscode-elements/elements)
// JSDOM does not support ResizeObserver, so we provide a no-op implementation
class ResizeObserverPolyfill {
Expand Down
139 changes: 134 additions & 5 deletions src/test/suite/views/data-browsing-app/monaco-viewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
// Mock the Monaco Editor component
let mockEditorValue = '';

// Minimal Monaco instance passed as the second argument to onMount.
const mockMonacoInstance = {
editor: { EditorOption: { lineHeight: 31 } },
KeyMod: { CtrlCmd: 2048 },
KeyCode: { KeyC: 33 },
};

const mockEditorInstance = {
getValue: (): string => mockEditorValue,
setValue: (value: string): void => {
Expand All @@ -24,16 +31,23 @@
getValue: (): string => mockEditorValue,
}),
getContentHeight: (): number => 100,
onDidContentSizeChange: (): { dispose: () => void } => ({
// getOption is used to read the real line height after mount.
getOption: (): number => 19,
// addAction is used to register the Ctrl+C keybinding.
addAction: (): { dispose: () => void } => ({
dispose: (): void => {
/* no-op */
},
}),
getAction: (): { run: () => void } => ({
run: (): void => {
onDidContentSizeChange: (): { dispose: () => void } => ({
dispose: (): void => {
/* no-op */
},
}),
// run() must return a Promise because the component chains .then() on it.
getAction: (): { run: () => Promise<void> } => ({
run: (): Promise<void> => Promise.resolve(),
}),
dispose: (): void => {
/* no-op */
},
Expand All @@ -44,9 +58,11 @@
React.useEffect(() => {
if (onMount && value) {
mockEditorValue = value;
// Simulate editor mount
// Simulate editor mount, passing both the editor instance and the monaco
// instance so the component's handleEditorMount doesn't error on the
// second argument (used for EditorOption.lineHeight, KeyMod, KeyCode).
setTimeout(() => {
onMount(mockEditorInstance);
onMount(mockEditorInstance, mockMonacoInstance);
}, 0);
}
}, [onMount, value]);
Expand Down Expand Up @@ -489,4 +505,117 @@
expect(deleteIcon).to.exist;
});
});

describe('Show more / Show less', function () {
// Builds a document with `count` top-level fields named field0..fieldN.
const makeDocument = (count: number): Record<string, unknown> =>
Object.fromEntries(
Array.from({ length: count }, (_, i) => [`field${i}`, i]),
);

it('should not show toggle buttons for documents with ≤ 15 fields', function () {
render(<MonacoViewer document={makeDocument(15)} themeKind="vs-dark" />);

expect(screen.queryByText(/Show .* more field/)).to.not.exist;
expect(screen.queryByText('Show less')).to.not.exist;
});

it('should show "Show N more fields" button for documents with > 15 fields', function () {
// 20 fields → 20 - 15 = 5 hidden → "Show 5 more fields"
render(<MonacoViewer document={makeDocument(20)} themeKind="vs-dark" />);

// Button text lives inside a <span>; use closest('button') for the
// data-expanded attribute which is set on the <button> element.
const btnText = screen.getByText(/Show 5 more fields/);
expect(btnText).to.exist;
expect(btnText.closest('button')?.getAttribute('data-expanded')).to.equal(
'false',
);
});

it('should use singular "field" when exactly one field is hidden', function () {
// 16 fields → 16 - 15 = 1 hidden → "Show 1 more field" (no trailing "s")
render(<MonacoViewer document={makeDocument(16)} themeKind="vs-dark" />);

expect(screen.getByText(/Show 1 more field/)).to.exist;
expect(screen.queryByText(/Show 1 more fields/)).to.not.exist;
});

it('should switch to "Show less" button after clicking "Show more"', function () {
render(<MonacoViewer document={makeDocument(20)} themeKind="vs-dark" />);

fireEvent.click(screen.getByText(/Show 5 more fields/));

// "Show less" text is inside a <span>; the data-expanded attribute is on
// the parent <button>.
const btnText = screen.getByText('Show less');
expect(btnText).to.exist;
expect(btnText.closest('button')?.getAttribute('data-expanded')).to.equal(
'true',
);
expect(screen.queryByText(/Show 5 more fields/)).to.not.exist;
});

it('should switch back to "Show more" button after clicking "Show less"', function () {
render(<MonacoViewer document={makeDocument(20)} themeKind="vs-dark" />);

fireEvent.click(screen.getByText(/Show 5 more fields/));
fireEvent.click(screen.getByText('Show less'));

expect(screen.getByText(/Show 5 more fields/)).to.exist;
expect(screen.queryByText('Show less')).to.not.exist;
});

it('should call window.scrollBy to restore button viewport position after collapsing', async function () {
// JSDOM may not have scrollBy — define a no-op so sinon can stub it.
if (!window.scrollBy) {
(window as any).scrollBy = () => {

Check warning on line 572 in src/test/suite/views/data-browsing-app/monaco-viewer.test.tsx

View workflow job for this annotation

GitHub Actions / Build and Check

Missing return type on function
/* no-op */
};
}
const scrollByStub = sinon.stub(window, 'scrollBy');

render(<MonacoViewer document={makeDocument(20)} themeKind="vs-dark" />);

// Expand so the "Show less" button is visible.
fireEvent.click(screen.getByText(/Show 5 more fields/));

// "Show less" text is inside a <span> inside a <button> inside the
// <div ref={buttonWrapperRef}>. We need to stub getBoundingClientRect
// on that outer <div>, which is what handleCollapse reads.
// 1st call (in handleCollapse, while expanded) → top = 500
// 2nd call (inside requestAnimationFrame, after collapse) → top = 200
const showLessSpan = screen.getByText('Show less');
const buttonWrapper = showLessSpan.closest('button')
?.parentElement as HTMLDivElement;
let rectCallCount = 0;
sinon
.stub(buttonWrapper, 'getBoundingClientRect')
.callsFake(
() => ({ top: rectCallCount++ === 0 ? 500 : 200 }) as DOMRect,
);

fireEvent.click(showLessSpan);

await waitFor(() => {
expect(scrollByStub.calledOnce).to.be.true;
// delta = newTop - targetTop = 200 - 500 = -300
expect(scrollByStub.calledWith(0, -300)).to.be.true;
});
});

it('should not call window.scrollBy when clicking "Show more"', function () {
if (!window.scrollBy) {
(window as any).scrollBy = () => {

Check warning on line 609 in src/test/suite/views/data-browsing-app/monaco-viewer.test.tsx

View workflow job for this annotation

GitHub Actions / Build and Check

Missing return type on function
/* no-op */
};
}
const scrollByStub = sinon.stub(window, 'scrollBy');

render(<MonacoViewer document={makeDocument(20)} themeKind="vs-dark" />);
fireEvent.click(screen.getByText(/Show 5 more fields/));

expect(scrollByStub.called).to.be.false;
});
});
});
Loading
Loading