0
0
mirror of https://github.com/wagtail/wagtail.git synced 2024-11-29 01:22:07 +01:00

fix: MutationObserver in dirty form check only tests direct descendants

Fixes #11142
This commit is contained in:
Karthik Ayangar 2024-03-01 21:44:53 +05:30 committed by LB (Ben Johnston)
parent caf9142c5d
commit 00474a6152
4 changed files with 105 additions and 6 deletions

View File

@ -22,6 +22,7 @@ Changelog
* Fix: Resolve issue local development of docs when running `make livehtml` (Sage Abdullah) * Fix: Resolve issue local development of docs when running `make livehtml` (Sage Abdullah)
* Fix: Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah) * Fix: Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah)
* Fix: Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues) * Fix: Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues)
* Fix: Ensure the Stimulus `UnsavedController` checks for nested removal/additions of inputs so that the unsaved warning shows in more valid cases when editing a page (Karthik Ayangar)
* Docs: Add contributing development documentation on how to work with a fork of Wagtail (Nix Asteri, Dan Braghis) * Docs: Add contributing development documentation on how to work with a fork of Wagtail (Nix Asteri, Dan Braghis)
* Docs: Make sure the settings panel is listed in tabbed interface examples (Tibor Leupold) * Docs: Make sure the settings panel is listed in tabbed interface examples (Tibor Leupold)
* Docs: Update content and page names to their US spelling instead of UK spelling (Victoria Poromon) * Docs: Update content and page names to their US spelling instead of UK spelling (Victoria Poromon)

View File

@ -112,6 +112,96 @@ describe('UnsavedController', () => {
expect(events['w-unsaved:add']).toHaveLength(1); expect(events['w-unsaved:add']).toHaveLength(1);
expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits'); expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits');
}); });
it('should allow checking for when an input is removed', async () => {
expect(events['w-unsaved:add']).toHaveLength(0);
await setup();
// setup should not fire any event
expect(events['w-unsaved:add']).toHaveLength(0);
const input = document.getElementById('name');
input.remove();
await jest.runAllTimersAsync();
expect(events['w-unsaved:add']).toHaveLength(1);
expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits');
});
it('should ignore when non-inputs are added', async () => {
expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events
await setup();
expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup
// Act (simulate the addition of a paragraph)
const paragraph = document.createElement('p');
paragraph.id = 'paraName';
paragraph.textContent = 'This is a new paragraph'; // Add some content for clarity
document.getElementsByTagName('form')[0].appendChild(paragraph); // paragraph is added
await jest.runAllTimersAsync();
// Assert (verify no events were fired)
expect(events['w-unsaved:add']).toHaveLength(0);
});
it('should fire an event when a textarea is added', async () => {
expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events
await setup();
expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup
// Act (simulate adding a textarea with value)
const textarea = document.createElement('textarea');
textarea.value = 'Some initial content';
textarea.id = 'taName';
document.getElementsByTagName('form')[0].appendChild(textarea);
await jest.runAllTimersAsync(); // Allow any timers to trigger
// Assert (verify event was fired)
expect(events['w-unsaved:add']).toHaveLength(1);
expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits');
});
it('should fire an event when a nested input (select) is added', async () => {
// Arrange
expect(events['w-unsaved:add']).toHaveLength(0); // Ensure no initial events
await setup();
expect(events['w-unsaved:add']).toHaveLength(0); // Verify no events after setup
// Act
const div = document.createElement('div');
const select = document.createElement('select');
select.id = 'mySelect';
const option1 = document.createElement('option');
option1.value = 'option1';
option1.textContent = 'Option 1';
select.appendChild(option1);
const option2 = document.createElement('option');
option2.value = 'option2';
option2.textContent = 'Option 2';
select.appendChild(option2);
div.appendChild(select);
document.body.getElementsByTagName('form')[0].appendChild(div);
await jest.runAllTimersAsync();
// Assert
expect(events['w-unsaved:add']).toHaveLength(1);
expect(events['w-unsaved:add'][0]).toHaveProperty('detail.type', 'edits');
});
}); });
describe('showing a confirmation message when exiting the browser tab', () => { describe('showing a confirmation message when exiting the browser tab', () => {

View File

@ -198,6 +198,17 @@ export class UnsavedController extends Controller<HTMLFormElement> {
if (current !== previous) this.notify(); if (current !== previous) this.notify();
} }
getIsValidNode(node: Node | null) {
if (!node || node.nodeType !== node.ELEMENT_NODE) return false;
const validElements = ['input', 'textarea', 'select'];
return (
validElements.includes((node as Element).localName) ||
(node as Element).querySelector(validElements.join(',')) !== null
);
}
/** /**
* Notify the user of changes to the form. * Notify the user of changes to the form.
* Dispatch events to update the footer message via dispatching events. * Dispatch events to update the footer message via dispatching events.
@ -288,15 +299,11 @@ export class UnsavedController extends Controller<HTMLFormElement> {
detail: { initialFormData }, detail: { initialFormData },
}); });
const isValidInputNode = (node) =>
node.nodeType === node.ELEMENT_NODE &&
['INPUT', 'TEXTAREA', 'SELECT'].includes(node.tagName);
const observer = new MutationObserver((mutationList) => { const observer = new MutationObserver((mutationList) => {
const hasMutationWithValidInputNode = mutationList.some( const hasMutationWithValidInputNode = mutationList.some(
(mutation) => (mutation) =>
Array.from(mutation.addedNodes).some(isValidInputNode) || Array.from(mutation.addedNodes).some(this.getIsValidNode) ||
Array.from(mutation.removedNodes).some(isValidInputNode), Array.from(mutation.removedNodes).some(this.getIsValidNode),
); );
if (hasMutationWithValidInputNode) this.check(); if (hasMutationWithValidInputNode) this.check();

View File

@ -35,6 +35,7 @@ depth: 1
* Resolve issue local development of docs when running `make livehtml` (Sage Abdullah) * Resolve issue local development of docs when running `make livehtml` (Sage Abdullah)
* Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah) * Resolve issue with unwanted padding in chooser modal listings (Sage Abdullah)
* Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues) * Ensure form builder emails that have date or datetime fields correctly localize dates based on the configured `LANGUAGE_CODE` (Mark Niehues)
* Ensure the Stimulus `UnsavedController` checks for nested removal/additions of inputs so that the unsaved warning shows in more valid cases when editing a page (Karthik Ayangar)
### Documentation ### Documentation