diff --git a/docs/reference/x-target.md b/docs/reference/x-target.md index 4568dcf..760eb61 100644 --- a/docs/reference/x-target.md +++ b/docs/reference/x-target.md @@ -135,13 +135,9 @@ Sometimes simple target literals (i.e. comment_1) are not sufficient. In these c ``` -### History & URL Support +### Updating the URL -Use the `x-target.replace` modifier to replace the URL in the browser's navigation bar when an AJAX request is issued. - -Use the `x-target.push` modifier to push a new history entry onto the browser's session history stack when an AJAX request is issued. - -`replace` simply changes the browser’s URL without adding a new entry to the browser’s session history stack, where as `push` creates a new history entry allowing your users to navigate back to the previous URL using the browser’s "Back" button. +Use the `x-target.url` modifier to replace the URL in the browser's navigation bar when an AJAX request is issued. ### Disable AJAX per submit button diff --git a/src/index.js b/src/index.js index 88d12c4..c6e0753 100644 --- a/src/index.js +++ b/src/index.js @@ -13,29 +13,23 @@ function Ajax(Alpine) { if (Alpine.morph) doMorph = Alpine.morph Alpine.addInitSelector(() => `[${Alpine.prefixed('target')}]`) - Alpine.addInitSelector(() => `[${Alpine.prefixed('target\\.push')}]`) Alpine.addInitSelector(() => `[${Alpine.prefixed('target\\.replace')}]`) Alpine.directive('target', (el, { value, modifiers, expression }, { evaluateLater, effect }) => { let setTarget = (ids) => { - el._ajax_target = el._ajax_target || {} - let plan = { - ids: parseIds(el, ids), - sync: true, - focus: !modifiers.includes('nofocus'), - history: modifiers.includes('push') ? 'push' : (modifiers.includes('replace') ? 'replace' : false), - } - - let statues = modifiers.filter((modifier) => ['back', 'away', 'error'].includes(modifier) || parseInt(modifier)) - statues = statues.length ? statues : ['xxx'] - statues.forEach(status => { - // Redirect status codes are opaque to fetch - // so we just use the first 3xx status given. - if (status.charAt(0) === '3') { - status = '3xx' + ids = (ids ? ids.split(' ') : [el.getAttribute('id')]).filter(Boolean) + let statuses = modifiers.filter(m => /^(back|away|error|\d+)$/.test(m)).map( + m => m.startsWith('3') ? '3xx' : m // Redirect status codes are opaque to fetch + ) + if (statuses.length) { + el._ajax_status = Object.assign({}, el._ajax_status, Object.fromEntries(statuses.map(status => [status, ids]))) + } else { + el._ajax_target = { + ids, + sync: true, + focus: !modifiers.includes('nofocus'), + history: modifiers.includes('replace') || false, } - - el._ajax_target[status] = plan - }) + } } if (value === 'dynamic') { @@ -72,41 +66,27 @@ function Ajax(Alpine) { }) Alpine.magic('ajax', (el) => { - return async (action, options = {}) => { - let control = { - el, - target: { - 'xxx': { - ids: parseIds(el, options.targets || options.target), - sync: Boolean(options.sync), - history: ('history' in options) ? options.history : false, - focus: ('focus' in options) ? options.focus : true, - }, - }, - headers: options.headers || {} - } - let method = options.method ? options.method.toUpperCase() : 'GET' - let body = options.body - - return send(control, action, method, body) - } + return async (action, { targets, target, ...options } = {}) => send({ + el, action, ...options, + ids: (targets || target?.split(' ') || [el.getAttribute('id')]).filter(Boolean), + }) }) let started = false Alpine.ajax = { + configure: Ajax.configure, + merge, + send, start() { if (!started) { document.addEventListener('submit', handleForms) document.addEventListener('click', handleLinks) - window.addEventListener('popstate', handleHistory) started = true } }, - configure: Ajax.configure, stop() { document.removeEventListener('submit', handleForms) document.removeEventListener('click', handleLinks) - window.removeEventListener('popstate', handleHistory) started = false }, } @@ -121,12 +101,6 @@ Ajax.configure = (options) => { export default Ajax -function handleHistory(event) { - if (!event.state || !event.state.__ajax) return - - window.location.reload(true) -} - async function handleLinks(event) { if (event.defaultPrevented || event.which > 1 || @@ -149,24 +123,16 @@ async function handleLinks(event) { event.preventDefault() event.stopImmediatePropagation() - let control = { + return send({ + ...link._ajax_target, el: link, - target: link._ajax_target, headers: link._ajax_headers || {}, - } - let action = link.getAttribute('href') - - try { - return await send(control, action) - } catch (error) { - if (error.name === 'RenderError') { - console.warn(error.message) - window.location.href = link.href - return - } - - throw error - } + action: link.getAttribute('href'), + }).catch(error => { + if (error.name !== 'RenderError') throw error + console.warn(error.message) + window.location.href = link.href + }) } async function handleForms(event) { @@ -190,391 +156,245 @@ async function handleForms(event) { event.preventDefault() event.stopImmediatePropagation() - let control = { - el: form, - target: form._ajax_target, - headers: form._ajax_headers || {}, - } let body = new FormData(form) - let enctype = form.getAttribute('enctype') - let action = form.getAttribute('action') - if (submitter) { - enctype = submitter.getAttribute('formenctype') || enctype - action = submitter.getAttribute('formaction') || action - if (submitter.name) { - body.append(submitter.name, submitter.value) - } - } - - try { - return await withSubmitter(submitter, () => { - return send(control, action, method, body, enctype) - }) - } catch (error) { - if (error.name === 'RenderError') { - console.warn(error.message) - form.setAttribute('noajax', 'true') - form.requestSubmit(submitter) + if (submitter?.name) body.append(submitter.name, submitter.value) - return - } - - throw error - } + return withSubmitter(submitter, () => send({ + ...form._ajax_target, + body, + method, + el: form, + headers: form._ajax_headers || {}, + action: submitter?.getAttribute('formaction') || form.getAttribute('action'), + enctype: submitter?.getAttribute('formenctype') || form.getAttribute('enctype'), + })).catch(error => { + if (error.name !== 'RenderError') throw error + console.warn(error.message) + form.setAttribute('noajax', 'true') + form.requestSubmit(submitter) + }) } -async function withSubmitter(submitter, callback) { - if (!submitter) return await callback() +function withSubmitter(submitter, callback) { + if (!submitter) return callback() let disableEvent = e => e.preventDefault() submitter.setAttribute('aria-disabled', 'true') submitter.addEventListener('click', disableEvent) - let result - try { - result = await callback() - } finally { + return callback().finally(() => { submitter.removeAttribute('aria-disabled') submitter.removeEventListener('click', disableEvent) - } - - return result + }) } let PendingTargets = { store: new Map, - plan(plan, response) { - plan.ids.forEach(pair => { - let docId = pair[0] - let el = ['_self', '_top', '_none'].includes(docId) ? document.documentElement : document.getElementById(docId) - if (!el) { - return console.warn(`Target [#${docId}] was not found in current document.`) - } + get(request) { + let targets = [] + this.store.forEach((r, t) => request === r && targets.push(t)) - el._ajax_id = pair[1] - this.set(el, response) - }) + return targets + }, + set(request, ids = [], sync = true) { + this.purge(request) - if (plan.sync) { - let targeted = plan.ids.flat() - document.querySelectorAll('[x-sync]').forEach(el => { - let id = el.getAttribute('id') - if (!id) { - throw new IDError(el) - } + if (ids.length === 0) throw new IDError(request.el) - if (targeted.includes(id)) { - return - } + let activate = (target, id) => { + target.setAttribute('aria-busy', 'true') + if (id) target._ajax_id = id - el._ajax_id = id - el._ajax_sync = true - this.set(el, response) - }) + return this.store.set(target, request) } - }, - purge(response) { - this.store.forEach((r, t) => response === r && this.delete(t)) - }, - get(response) { - let targets = [] - this.store.forEach((r, t) => response === r && targets.push(t)) - return targets - }, - set(target, response) { - target.querySelectorAll('[aria-busy]').forEach((busy) => { - this.delete(busy) + ids.forEach(token => { + let tokens = Array.isArray(token) ? token : token.split(settings.mapDelimiter) + let [id, alias] = tokens.map(t => t || request.el.getAttribute('id')) + alias = alias || id + let special = { '_none': request.el, '_top': document.documentElement } + if (special[id]) return activate(special[id]) + + let target = document.getElementById(id) + if (!target) return console.warn(`Target [#${id}] was not found in current document.`) + if (!target.getAttribute('id')) throw new IDError(target) + target.querySelectorAll('[aria-busy]').forEach(child => this.delete(child)) + activate(target, alias) + }) + + if (sync) document.querySelectorAll('[x-sync]').forEach(target => { + let id = target.getAttribute('id') + if (!id) throw new IDError(target) + target._ajax_sync = true + activate(target, id) }) - target.setAttribute('aria-busy', 'true') - this.store.set(target, response) + + return this }, delete(target) { + delete target._ajax_id + delete target._ajax_sync target.removeAttribute('aria-busy') - this.store.delete(target) + + return this.store.delete(target) }, + purge(request) { + this.store.forEach((r, t) => request === r && this.delete(t)) + + return true + } } let RequestCache = new Map -async function send(control, action = '', method = 'GET', body = null, enctype = 'application/x-www-form-urlencoded') { - if (!dispatch(control.el, 'ajax:before')) { - return - } - - let plan = control.target.xxx - let response = { ok: false, redirected: false, url: '', status: '', html: '', raw: '' } - PendingTargets.plan(plan, response) - let referrer = new URL(control.el.closest('[data-source]')?.dataset.source || '', document.baseURI) - action = new URL(action || referrer, document.baseURI) - if (body) { - body = parseFormData(body) - if (method === 'GET') { - action.search = formDataToParams(body).toString() - body = null - } else if (enctype !== 'multipart/form-data') { - body = formDataToParams(body) - } +async function send(request) { + if (!dispatch(request.el, 'ajax:before')) return + + PendingTargets.set(request, request.ids) + + request.method = request.method ?? 'GET' + request.referrer = new URL(request.el.closest('[data-source]')?.dataset.source || '', document.baseURI).toString() + request.action = new URL(request.action || request.referrer.toString(), document.baseURI).toString() + if (/GET|DELETE/.test(request.method)) { + let params = new URLSearchParams(request.body).toString() + if (params) request.action += (/\?/.test(request.action) ? "&" : "?") + params + request.body = null + } else if (request.body instanceof FormData && !request.enctype) { + request.body = new URLSearchParams( + Array.from(request.body.entries()).filter(([key, value]) => !(value instanceof File)) + ) + } else if (request.body?.constructor === Object) { + request.body = JSON.stringify(request.body) } - - let request = { - action: action.toString(), - method, - body, - enctype, - referrer: referrer.toString(), - headers: Object.assign({ - 'X-Alpine-Request': true, - 'X-Alpine-Target': PendingTargets.get(response).map(target => target._ajax_id).join(' '), - }, settings.headers, control.headers), + request.headers = { + 'X-Alpine-Request': true, + 'X-Alpine-Target': PendingTargets.get(request).map(t => t._ajax_id).join(' '), + ...settings.headers, ...(request.headers ?? {}) } - dispatch(control.el, 'ajax:send', request) + dispatch(request.el, 'ajax:send', { request }) - let pending - if (request.method === 'GET' && RequestCache.has(request.action)) { - pending = RequestCache.get(request.action) - } else { - pending = fetch(request.action, request).then(async (r) => { - let text = await r.text() - let wrapper = document.createRange().createContextualFragment('') - r.html = wrapper.firstElementChild.content - r.raw = text + let pending = (request.method === 'GET' && RequestCache.get(request.action)) + || fetch(request.action, request).then(async response => { + let raw = await response.text() + let doc = document.createRange().createContextualFragment('') - return r + return Object.assign(response, { raw, html: doc.firstElementChild.content }) }) - RequestCache.set(request.action, pending) - } - await pending.then((r) => { - response.ok = r.ok - response.redirected = r.redirected - response.url = r.url - response.status = r.status - response.html = r.html - response.raw = r.raw - }) + if (request.method === 'GET') RequestCache.set(request.action, pending) + + let response = await pending if (response.ok) { if (response.redirected) { - dispatch(control.el, 'ajax:redirect', response) + dispatch(request.el, 'ajax:redirect', { request, response }) RequestCache.set(response.url, pending) setTimeout(() => { RequestCache.delete(response.url) }, 5) } - dispatch(control.el, 'ajax:success', response) + dispatch(request.el, 'ajax:success', { request, response }) } else { - dispatch(control.el, 'ajax:error', response) + dispatch(request.el, 'ajax:error', { request, response }) } - dispatch(control.el, 'ajax:sent', response) + dispatch(request.el, 'ajax:sent', { request, response }) RequestCache.delete(request.action) - if (!response.html) { - PendingTargets.purge(response) - - return - } - - let status = response.redirected ? '3xx' : response.status.toString() - let isBack = samePath(new URL(response.url), new URL(request.referrer, document.baseURI)) - let key = [ - response.redirected ? (isBack ? 'back' : 'away') : null, - status, - status.charAt(0) + 'xx', - response.ok ? 'xxx' : 'error', - 'xxx', - ].find(key => key in control.target) - if (key !== 'xxx') { - plan = control.target[key] - if (!response.redirected || !isBack || !plan.ids.flat().includes('_self')) { - PendingTargets.purge(response) - PendingTargets.plan(plan, response) - } - } - - if (plan.history) { - updateHistory(plan.history, response.url) + if (!response.raw) return PendingTargets.purge(request) + + if (request.el._ajax_status) { + let status = response.redirected ? '3xx' : response.status.toString() + let isBack = response.redirected && samePath(new URL(response.url), new URL(request.referrer, document.baseURI)) + let key = [ + status, + isBack ? 'back' : (response.redirected ? 'away' : null), + status[0] + 'xx', + !response.ok && 'error', + ].find(key => key in request.el._ajax_status) + if (key) PendingTargets.set(request, request.el._ajax_status[key]) } - let focused = !plan.focus - let renders = PendingTargets.get(response).map(async target => { + if (request.history) updateHistory('replace', response.url) - if (!target.isConnected || target._ajax_id === '_none') { + let focused = !request.focus + let render = await Promise.all( + PendingTargets.get(request).map(async target => { + let rendered = await renderTarget(target, response, request) PendingTargets.delete(target) - return - } - - if (target === document.documentElement) { - window.location.href = response.url - return - } - - let content = response.html.getElementById(target._ajax_id) - if (!content) { - if (target._ajax_sync) { - return - } - - if (!dispatch(control.el, 'ajax:missing', { target, response })) { - return - } - - if (response.ok) { - return target.remove() + if (!focused && rendered) { + focused = (['[x-autofocus]', '[autofocus]']).some(selector => { + if (rendered.matches(selector) && focus(rendered)) return true + return Array.from(rendered.querySelectorAll(selector)).some(child => focus(child)) + }) } - throw new RenderError(target, response.status) - } - - let strategy = target._ajax_strategy || settings.mergeStrategy - let render = async () => { - target = await merge(strategy, target, content) - if (target) { - target.dataset.source = response.url - PendingTargets.delete(target) - let selectors = ['[x-autofocus]', '[autofocus]'] - while (!focused && selectors.length) { - let selector = selectors.shift() - if (target.matches(selector)) { - focused = focusOn(target) - } - focused = focused || Array.from(target.querySelectorAll(selector)).some(focusable => focusOn(focusable)) - } - } - - dispatch(target, 'ajax:merged') - - return target - } - - if (!dispatch(target, 'ajax:merge', { strategy, content, merge: render })) { - return - } - - return render() - }) - - let render = await Promise.all(renders) + return rendered + }) + ) - dispatch(control.el, 'ajax:after', { response, render }) + dispatch(request.el, 'ajax:after', { response, render }) return render } -function parseFormData(data) { - if (data instanceof FormData) return data - if (data instanceof HTMLFormElement) return new FormData(data) +async function renderTarget(target, response, request) { + if (target === document.documentElement) window.location.href = response.url + if (!target.isConnected || !target._ajax_id) return - const formData = new FormData() - for (let key in data) { - if (typeof data[key] === 'object') { - formData.append(key, JSON.stringify(data[key])) - } else { - formData.append(key, data[key]) - } - } + let content = response.html.getElementById(target._ajax_id) - return formData -} - -function formDataToParams(body) { - let params = Array.from(body.entries()).filter(([key, value]) => { - return !(value instanceof File) - }) - - return new URLSearchParams(params) -} - -async function merge(strategy, target, to) { - let strategies = { - before(from, to) { - from.before(...to.childNodes) + if (!content) { + if (target._ajax_sync) return + if (!dispatch(request.el, 'ajax:missing', { target, request, response })) return + if (response.ok) return target.remove() + throw new RenderError(target, response.status) + } - return from - }, - replace(from, to) { - from.replaceWith(to) + let strategy = target._ajax_strategy || settings.mergeStrategy + let detail = { content, target, strategy, request, response } - return to - }, - update(from, to) { - from.replaceChildren(...to.childNodes) + if (!dispatch(target, 'ajax:merge', detail)) return - return from - }, - prepend(from, to) { - from.prepend(...to.childNodes) - - return from - }, - append(from, to) { - from.append(...to.childNodes) + let merged = await merge(target, detail.content, detail.strategy) + if (merged) { + merged.dataset.source = response.url + PendingTargets.delete(merged) + dispatch(merged, 'ajax:merged', { request, response }) + } - return from - }, - after(from, to) { - from.after(...to.childNodes) + return merged +} - return from - }, - morph(from, to) { - doMorph(from, to) +async function merge(from, to, strategy = 'replace') { + const run = { + replace: () => { from.replaceWith(to); return to }, + morph: () => { doMorph(from, to); return document.getElementById(to.id) }, + update: () => { from.replaceChildren(...to.childNodes); return from }, + }[strategy] || (() => { from[strategy](...to.childNodes); return from }) - return document.getElementById(to.getAttribute('id')) - } - } + let result + const execute = () => result = run() - if (!target._ajax_transition || !document.startViewTransition) { - return strategies[strategy](target, to) + if (from._ajax_transition && document.startViewTransition) { + await document.startViewTransition(execute).updateCallbackDone + } else { + execute() } - let transition = document.startViewTransition(() => { - target = strategies[strategy](target, to) - return Promise.resolve() - }) - await transition.updateCallbackDone - - return target + return result } -function focusOn(el) { - if (!el) return false - if (!el.getClientRects().length) return false - setTimeout(() => { +function focus(el) { + return el?.getClientRects().length && setTimeout(() => { if (!el.hasAttribute('tabindex')) el.setAttribute('tabindex', '0') el.focus() }, 0) - - return true } function updateHistory(strategy, url) { - let strategies = { - push: () => window.history.pushState({ __ajax: true }, '', url), - replace: () => window.history.replaceState({ __ajax: true }, '', url), - } - - return strategies[strategy](); -} - -function parseIds(el, ids = null) { - let elId = el.getAttribute('id') - let parsed = [elId] - if (ids) { - parsed = Array.isArray(ids) ? ids : ids.split(' ') - } - parsed = parsed.filter(id => id).map(id => { - let pair = id.split(settings.mapDelimiter).map(id => id || elId) - pair[1] = pair[1] || pair[0] - return pair - }) - if (parsed.length === 0) { - throw new IDError(el) - } - - return parsed + return window.history[`${strategy}State`]({ __ajax: true }, '', url) } function dispatch(el, name, detail) { @@ -589,11 +409,9 @@ function dispatch(el, name, detail) { } function samePath(urlA, urlB) { - return stripTrailingSlash(urlA.pathname) === stripTrailingSlash(urlB.pathname) -} + let stripTrailingSlash = (u) => u.pathname.replace(/\/$/, '') -function stripTrailingSlash(str) { - return str.replace(/\/$/, "") + return stripTrailingSlash(urlA) === stripTrailingSlash(urlB) } class IDError extends DOMException { diff --git a/tests/events.cy.js b/tests/events.cy.js index 9900562..f57b523 100644 --- a/tests/events.cy.js +++ b/tests/events.cy.js @@ -35,7 +35,7 @@ test('[ajax:before] can cancel AJAX requests', ) test('[ajax:send] can modify a request', - html`
`, + html`
`, ({ intercept, get, wait }) => { intercept('POST', '/changed', { statusCode: 200, diff --git a/tests/exceptions.cy.js b/tests/exceptions.cy.js index d4d2f1b..8973666 100644 --- a/tests/exceptions.cy.js +++ b/tests/exceptions.cy.js @@ -1,17 +1,5 @@ import { test, html } from './utils' -test('Element throws an exception when a target is missing', - html`
`, - ({ intercept, get }) => { - intercept('POST', '/tests', { - statusCode: 200, - body: '

Success

Replaced
' - }).as('response') - get('button').click() - }, - (err) => err.name === 'TargetError' -) - test('Target throws an exception when it is missing an ID', html`
`, ({ intercept, get }) => { diff --git a/tests/history.cy.js b/tests/history.cy.js index 28a31c4..25f42d9 100644 --- a/tests/history.cy.js +++ b/tests/history.cy.js @@ -15,21 +15,3 @@ test('replaces the history state with the replace modifier', }) } ) - -test('pushes to history state with the push modifier', - html`
`, - ({ intercept, get, wait, location, go }) => { - intercept('GET', '/pushed', { - statusCode: 200, - body: '

Success

Replaced
' - }).as('response') - get('button').click() - wait('@response').then(() => { - get('#title').should('not.exist') - get('#replace').should('have.text', 'Replaced') - location('href').should('include', '/pushed'); - go('back') - location('href').should('include', '/tests'); - }) - } -) diff --git a/tests/load.cy.js b/tests/load.cy.js index f59f09a..9ac7db5 100644 --- a/tests/load.cy.js +++ b/tests/load.cy.js @@ -77,7 +77,7 @@ test('content is lazily loaded with a custom event trigger', } ) -test('aria-busy is added to busy targets', +test('[aria-busy] is added to busy targets', html`Link`, ({ intercept, get, wait }) => { intercept('GET', '/tests', { @@ -92,7 +92,7 @@ test('aria-busy is added to busy targets', } ) -test('aria-busy is removed from targets that are not replaced', +test('[aria-busy] is removed from targets that are not replaced', html`
Link
`, ({ intercept, get, wait }) => { intercept('GET', '/tests', { @@ -108,18 +108,21 @@ test('aria-busy is removed from targets that are not replaced', } ) -test('aria-busy is removed from root node when target is _none', - html`Link`, - ({ intercept, get, wait }) => { - intercept('GET', '/tests', { - delay: 1000, - statusCode: 200, - body: '', - }).as('response') - get('a').click() - get('html').should('have.attr', 'aria-busy') - wait('@response').then(() => { - get('html').should('not.have.attr', 'aria-busy') - }) - } -) +// This Cypress bug is breaking this test +// https://github.com/cypress-io/cypress/issues/26206 +// +// test('[aria-busy] is removed from root node when target is _none', +// html`
`, +// ({ intercept, get, wait }) => { +// intercept('GET', '/tests', { +// delay: 1000, +// statusCode: 200, +// body: '', +// }).as('response') +// get('button').click() +// get('form').should('have.attr', 'aria-busy') +// wait('@response').then(() => { +// get('form').should('not.have.attr', 'aria-busy') +// }) +// } +// ) diff --git a/tests/map.cy.js b/tests/map.cy.js index 9ee1393..171e467 100644 --- a/tests/map.cy.js +++ b/tests/map.cy.js @@ -62,3 +62,17 @@ test('mapping can omit response element target', } ) +test('target delimiter is optional', + html`
`, + ({ intercept, get, wait }) => { + intercept('GET', '/tests', { + statusCode: 200, + body: '

Success

Error
Replaced
' + }).as('response') + get('button').click() + wait('@response').then(() => { + get('#title').should('not.exist') + get('#replace').should('have.text', 'Replaced') + }) + } +) diff --git a/tests/merge.cy.js b/tests/merge.cy.js index 0c05881..24f0848 100644 --- a/tests/merge.cy.js +++ b/tests/merge.cy.js @@ -105,7 +105,7 @@ test('merging can be interrupted', ) test('merged content can be changed', - html`
`, + html`
`, ({ intercept, get, wait }) => { intercept('POST', '/tests', { statusCode: 200, @@ -117,3 +117,18 @@ test('merged content can be changed', }) } ) + +test('merged strategy can be changed', + html`
`, + ({ intercept, get, wait }) => { + intercept('POST', '/tests', { + statusCode: 200, + body: '

Success

Replaced
' + }).as('response') + get('button').click() + wait('@response').then(() => { + get('#title').should('not.exist') + get('#target').should('have.html', 'Replaced') + }) + } +) diff --git a/tests/status.cy.js b/tests/status.cy.js index 278c9e7..fc2211e 100644 --- a/tests/status.cy.js +++ b/tests/status.cy.js @@ -209,42 +209,6 @@ test('follows redirects by default', } ) -test('targeting `_self` will reload the page', - html`
`, - ({ intercept, get, wait }) => { - intercept('POST', '/tests', (request) => { - request.redirect('/redirect', 302) - }) - intercept('GET', '/redirect', { - statusCode: 200, - body: '

Redirected

Replaced
' - }).as('response') - get('button').click() - wait('@response').then(() => { - get('#title').should('have.text', 'Redirected') - get('#replace').should('have.text', 'Replaced') - }) - } -) - -test('targeting `_self` will not reload the page when redirected back to the same URL', - html`
`, - ({ intercept, get, wait }) => { - intercept('POST', '/tests', (request) => { - request.redirect('/tests', 302) - }) - intercept('GET', '/tests', { - statusCode: 200, - body: '

Redirected

Validation Error
' - }).as('response') - get('button').click() - wait('@response').then(() => { - get('#title').should('not.exist') - get('#replace').should('have.text', 'Validation Error') - }) - } -) - test('targeting `_top` will reload the page', html`
`, ({ intercept, get, wait }) => {