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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@
},
"dependencies": {
"@date-fns/tz": "1.4.1",
"@digdir/designsystemet-css": "1.11.1",
"@digdir/designsystemet-react": "1.11.1",
"@digdir/designsystemet-css": "1.18.0",
"@digdir/designsystemet-react": "1.18.0",
"@navikt/aksel-icons": "8.10.5",
"@tanstack/react-query": "5.100.10",
"@terraformer/wkt": "2.2.1",
Expand Down
3 changes: 1 addition & 2 deletions src/components/atoms/AltinnAttachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Link, List } from '@digdir/designsystemet-react';
import cn from 'classnames';

import classes from 'src/components/atoms/AltinnAttachment.module.css';
import { MainAttachmentHeader } from 'src/components/atoms/AttachmentHeader';
import { Lang } from 'src/features/language/Lang';
import { useCurrentLanguage } from 'src/features/language/LanguageProvider';
import { useLanguage } from 'src/features/language/useLanguage';
Expand Down Expand Up @@ -39,7 +38,7 @@ export function AltinnAttachments({
id={id}
data-testid='attachment-list'
>
<MainAttachmentHeader title={title} />
{title}
<List.Unordered
className={classes.attachmentList}
data-size='sm'
Expand Down
9 changes: 7 additions & 2 deletions src/features/instantiate/containers/UnknownError.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ describe('Unknown error', () => {

expect(console.error).not.toHaveBeenCalled();

const showDetailsButton = screen.getByRole('button', { name: 'Vis detaljer om feilen' });
await user.click(showDetailsButton);
const detailsGroup = screen.getByRole('group');
expect(detailsGroup).toHaveTextContent('Vis detaljer om feilen');
const summary = detailsGroup.querySelector('summary');
if (!summary) {
throw new Error('Expected a <summary> inside the details group');
}
await user.click(summary);
expect(screen.getByText('Error test message')).toBeInTheDocument();

const writeTextMock = jest.spyOn(navigator.clipboard, 'writeText').mockResolvedValue();
Expand Down
8 changes: 4 additions & 4 deletions src/layout/Accordion/Accordion.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ describe('Accordion', () => {
it('should display text from textResourceBindings', async () => {
await render({ title: 'Accordion title' });

expect(await screen.findByRole('button', { name: /accordion title/i })).toBeInTheDocument();
expect(await screen.findByRole('group')).toHaveTextContent(/accordion title/i);
});

it('should display text from textResourceBindings if an ID to a text resource is used as title', async () => {
await render({ title: 'accordion.title' });

expect(await screen.findByRole('button', { name: /this is a title/i })).toBeInTheDocument();
expect(await screen.findByRole('group')).toHaveTextContent(/this is a title/i);
});

it('should open accordion by default if openByDefault is set to true', async () => {
await render({ openByDefault: true, title: 'accordion.title' });
expect(await screen.findByRole('button', { name: /this is a title/i })).toHaveAttribute('aria-expanded', 'true');
expect(await screen.findByRole('group')).toHaveAttribute('open');
});

it('accordion should be closed by default if openByDefault is set to false', async () => {
await render({ openByDefault: false, title: 'accordion.title' });
expect(await screen.findByRole('button', { name: /this is a title/i })).toHaveAttribute('aria-expanded', 'false');
expect(await screen.findByRole('group')).not.toHaveAttribute('open');
});
});

Expand Down
3 changes: 2 additions & 1 deletion src/layout/AttachmentList/AttachmentListComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import { AltinnAttachments } from 'src/components/atoms/AltinnAttachments';
import { MainAttachmentHeader } from 'src/components/atoms/AttachmentHeader';
import { AttachmentGroupings } from 'src/components/organisms/AttachmentGroupings';
import { useApplicationMetadata } from 'src/features/applicationMetadata/ApplicationMetadataProvider';
import { useInstanceDataElements } from 'src/features/instance/InstanceContext';
Expand Down Expand Up @@ -82,7 +83,7 @@ export function AttachmentListComponent({ baseComponentId }: PropsFromGenericCom
) : (
<AltinnAttachments
attachments={displayAttachments}
title={title}
title={title ? <MainAttachmentHeader title={title} /> : undefined}
showLinks={showLinks}
showDescription={showDescription}
/>
Expand Down
39 changes: 38 additions & 1 deletion src/layout/Dropdown/DropdownComponent.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import { jest } from '@jest/globals';
import { act, screen, waitFor } from '@testing-library/react';
import { act, fireEvent, screen, waitFor } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import type { AxiosResponse } from 'axios';

Expand Down Expand Up @@ -104,6 +104,43 @@ describe('DropdownComponent', () => {
);
});

it('should commit the new value after confirming alertOnChange', async () => {
const { formDataMethods } = await render({
component: {
alertOnChange: true,
},
options: countries,
queries: {
fetchFormData: async () => ({
...getFormDataMockForRepGroup(),
myDropdown: 'norway',
}),
},
});

expect(await screen.findByRole('combobox')).toHaveValue('Norway');

await userEvent.click(screen.getByRole('combobox'));
await userEvent.click(screen.getByRole('option', { name: /sweden/i }));

// Mimic the alert popover stealing focus, which blurs the combobox and reverts the input display
// back to the previously-committed value before the change is confirmed.
fireEvent.blur(screen.getByRole('combobox'));

// Alert should appear; confirm the change
const confirmButton = await screen.findByRole('button', { name: /bekreft|confirm/i });
await userEvent.click(confirmButton);

await waitFor(() =>
expect(formDataMethods.setLeafValue).toHaveBeenCalledWith({
reference: { field: 'myDropdown', dataType: defaultDataTypeMock },
newValue: 'sweden',
}),
);

expect(screen.getByRole('combobox')).toHaveValue('Sweden');
});

it('should show as readonly when readOnly is true', async () => {
await render({
component: {
Expand Down
4 changes: 4 additions & 0 deletions src/layout/Dropdown/DropdownComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ export function DropdownComponent({ baseComponentId, overrideDisplay }: PropsFro
</DSLabel>
)}
<Suggestion
// With alertOnChange the commit is deferred until the user confirms, which breaks the DS combobox's
// runtime re-sync of the displayed input value. Keying by the committed value
// forces a fresh mount on commit.
key={alertOnChange ? `${id}-${selectedValues[0] ?? ''}` : undefined}
multiple={false}
filter={(args) => optionFilter(args, selectedLabels)}
data-size='sm'
Expand Down
5 changes: 5 additions & 0 deletions src/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ Element.prototype.getClientRects = () => ({
[Symbol.iterator]: jest.fn(),
});

// jsdom doesn't implement elementFromPoint, which Designsystemet's Popover uses
// in its `isTopLayer` check when handling Escape keydown. Returning the open
// popover element makes the check pass so ESC actually closes the popover in tests.
document.elementFromPoint = () => document.querySelector('[popover]');

// Forcing a low timeout for useDelayedSaveState()
global.delayedSaveState = 50;

Expand Down
30 changes: 14 additions & 16 deletions test/e2e/integration/frontend-test/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,22 +479,17 @@ describe('UI Components', () => {
cy.gotoHiddenPage('label-data-bindings');

cy.get('#form-content-colorsCheckboxes').click();
// After selecting an option, the DS combobox renders a removable chip (a <data role="option">) whose
// accessible name is "<label>, Press to remove". Selection state (aria-selected) lives on the list
// option, not the chip, so we assert the chip's presence to confirm the value was added.
cy.findByRole('option', { name: /blå/i }).click();
cy.findAllByRole('option', { name: /added blå, blå/i })
.last()
.should('have.attr', 'aria-selected', 'true');
cy.findByRole('option', { name: /Blå, Press to remove/i }).should('exist');
cy.findByRole('option', { name: /cyan/i }).click();
cy.findAllByRole('option', { name: /added cyan, cyan/i })
.last()
.should('have.attr', 'aria-selected', 'true');
cy.findByRole('option', { name: /Cyan, Press to remove/i }).should('exist');
cy.findByRole('option', { name: /grønn/i }).click();
cy.findAllByRole('option', { name: /added grønn, grønn/i })
.last()
.should('have.attr', 'aria-selected', 'true');
cy.findByRole('option', { name: /Grønn, Press to remove/i }).should('exist');
cy.findByRole('option', { name: /gul/i }).click();
cy.findAllByRole('option', { name: /added gul, gul/i })
.last()
.should('have.attr', 'aria-selected', 'true');
cy.findByRole('option', { name: /Gul, Press to remove/i }).should('exist');

cy.findByRole('option', {
name: /Grønn, Press to remove/i,
Expand Down Expand Up @@ -625,13 +620,16 @@ describe('UI Components', () => {
});

cy.goto('changename');
cy.get('#form-content-newFirstName').contains(`Du har ${maxLength} tegn igjen`);
// The DS field counter renders its text via CSS (`content: attr(data-label)`), so it is not part of the
// DOM text content and cannot be matched with .contains(). Assert on the data-label attribute instead.
const counter = '#form-content-newFirstName [data-field="counter"]';
cy.get(counter).should('have.attr', 'data-label', `Du har ${maxLength} tegn igjen`);
cy.get(appFrontend.changeOfName.newFirstName).type('Per');
cy.get('#form-content-newFirstName').contains(`Du har ${maxLength - 3} tegn igjen`);
cy.get(counter).should('have.attr', 'data-label', `Du har ${maxLength - 3} tegn igjen`);
cy.get(appFrontend.changeOfName.newFirstName).type('r');
cy.get('#form-content-newFirstName').contains(`Du har ${maxLength - 4} tegn igjen`);
cy.get(counter).should('have.attr', 'data-label', `Du har ${maxLength - 4} tegn igjen`);
cy.get(appFrontend.changeOfName.newFirstName).type('rrr');
cy.get('#form-content-newFirstName').contains(`Du har overskredet maks antall tegn med ${7 - maxLength}`);
cy.get(counter).should('have.attr', 'data-label', `Du har overskredet maks antall tegn med ${7 - maxLength}`);

// Display data model validation below component if maxLength in layout and datamodel is different
if (maxLength !== 4) {
Expand Down
6 changes: 0 additions & 6 deletions test/e2e/integration/frontend-test/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,6 @@ describe('Summary', () => {
});

it('is possible to view summary of repeating group', () => {
Cypress.on('uncaught:exception', (err) => {
if (err.message.includes('ResizeObserver loop completed with undelivered notifications')) {
return false;
}
});

cy.goto('group');

// Verify empty group summary
Expand Down
10 changes: 7 additions & 3 deletions test/e2e/support/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ Cypress.Commands.add('dsClear', (selector) => {
// Clear the input value and trigger input event to reset dropdown's internal state
cy.get(selector).invoke('val', '').trigger('input');

// Close any open dropdowns by clicking outside
cy.get('body').click('bottomRight');
// Close any open dropdowns by clicking outside.
// force:true because this is an outside-click to dismiss the dropdown, not a real user interaction — without
// it Cypress can fail with "<body> is covered by <html>" when the body's corner pixel resolves to <html>.
cy.get('body').click('bottomRight', { force: true });

// Additional step to ensure dropdown is reset
cy.get(selector).click();
Expand All @@ -106,7 +108,9 @@ Cypress.Commands.add('dsSelect', (selector, value, debounce = true) => {
// https://github.com/testing-library/cypress-testing-library/issues/205#issuecomment-974688283
cy.findByRole('option', { name: value }).click();
if (debounce) {
cy.get('body').click('bottomRight');
// force:true because this is an outside-click to dismiss the dropdown, not a real user interaction — without
// it Cypress can fail with "<body> is covered by <html>" when the body's corner pixel resolves to <html>.
cy.get('body').click('bottomRight', { force: true });
}
});

Expand Down
11 changes: 11 additions & 0 deletions test/e2e/support/fail-on-console-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,16 @@ export const ignoredConsoleMessages = [
// https://github.com/Altinn/app-frontend-react/issues/1851
/Warning: validateDOMNesting.*?Add a <tbody>, <thead> or <tfoot> to your code to match the DOM tree generated by the browser/,

// Designsystemet 1.15.0 deprecated RovingFocusRoot / RovingFocusItem / useRovingFocus without
// shipping a replacement. Used by LanguageSelector — remove once dsg provides a successor, or we have refactored it out.
// https://github.com/Altinn/app-frontend-react/issues/4240
/^Designsystemet: RovingFocus(Root|Item) is deprecated/,
/^Designsystemet: useRovingFocus is deprecated/,

/AxiosError: Request failed with status code 400/,

// Benign browser notification (not a real error) triggered by the Designsystemet combobox
// (EXPERIMENTAL_Suggestion / u-datalist) when its popover opens. Chrome logs this to the console in
// addition to dispatching it as an uncaught exception (the latter is handled in index.ts).
/ResizeObserver loop completed with undelivered notifications/,
];
9 changes: 9 additions & 0 deletions test/e2e/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ before(() => {
chai.use(chaiExtensions);
});

// "ResizeObserver loop completed with undelivered notifications" is a benign browser notification (not a real error)
// that the Designsystemet combobox (EXPERIMENTAL_Suggestion / u-datalist) triggers when its popover opens. Cypress
// fails any test on an uncaught application exception, so we have to explicitly ignore this specific message.
Cypress.on('uncaught:exception', (err) => {
if (err.message.includes('ResizeObserver loop completed with undelivered notifications')) {
return false;
}
});

// Clear media emulation and reset default command timeout before each test
beforeEach(() => {
cy.setEmulatedMedia();
Expand Down
5 changes: 5 additions & 0 deletions webpack.config.development.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ module.exports = {
overlay: {
errors: enableErrorsOverlay,
warnings: false,
// "ResizeObserver loop completed with undelivered notifications" is a benign browser notification (not a real
// error) triggered by the Designsystemet combobox's floating-positioning logic. Don't let it raise the
// full-screen dev-server error overlay, which renders as an iframe that covers the page and breaks e2e tests.
runtimeErrors: (error) =>
!error?.message?.includes('ResizeObserver loop completed with undelivered notifications'),
},
},
static: [
Expand Down
Loading
Loading