diff --git a/e2e/hal-explorer.spec.ts b/e2e/hal-explorer.spec.ts index 9e7d9845..1a85fadb 100644 --- a/e2e/hal-explorer.spec.ts +++ b/e2e/hal-explorer.spec.ts @@ -39,7 +39,11 @@ test.describe('HAL Explorer App', () => { }); test('should display HAL sections when rendering users resource', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/movies.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/movies.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); await expect(page.locator('h5:has-text("JSON Properties")').first()).toBeVisible(); @@ -51,7 +55,11 @@ test.describe('HAL Explorer App', () => { }); test('should display only Links section when rendering root api', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/index.hal.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/index.hal.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); await expect(page.locator('text="JSON Properties"').first()).not.toBeVisible(); @@ -62,7 +70,11 @@ test.describe('HAL Explorer App', () => { }); test('should display POST request dialog', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/movies.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/movies.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the HAL-FORMS Template Elements section to be loaded @@ -81,7 +93,11 @@ test.describe('HAL Explorer App', () => { }); test('should display user profile in POST request dialog', { tag: '@flaky' }, async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/index.hal.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/index.hal.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the links section to be fully loaded @@ -114,7 +130,11 @@ test.describe('HAL Explorer App', () => { }); test('should display expanded URI in HAL-FORMS GET request dialog', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/filter.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/filter.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the HAL-FORMS section to be loaded @@ -141,7 +161,11 @@ test.describe('HAL Explorer App', () => { }); test('should close modal on ESC key', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/filter.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/filter.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the HAL-FORMS section to be loaded @@ -170,7 +194,11 @@ test.describe('HAL Explorer App', () => { }); test('should submit request on Enter key in parameterized GET request dialog', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/filter.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/filter.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the HAL-FORMS section to be loaded @@ -202,11 +230,15 @@ test.describe('HAL Explorer App', () => { await expect(modal).not.toHaveClass(/show/, { timeout: 5000 }); // Verify the URL was updated with the title parameter (meaning the request was made) - await expect(page).toHaveURL(/title=myTitle/); + expect(await page.evaluate(() => sessionStorage.getItem('hash'))).toContain('title=myTitle'); }); test('should display correct properties HAL-FORMS POST request dialog', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/2posts1get.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/2posts1get.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Click the first POST button (Post 1 template) @@ -225,7 +257,11 @@ test.describe('HAL Explorer App', () => { test('should update URI input field when clicking a link', async ({ page }) => { // Navigate to the root API which has links - await page.goto('/#uri=http://localhost:3000/index.hal.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/index.hal.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Verify the initial URI is displayed in the input field @@ -237,7 +273,9 @@ test.describe('HAL Explorer App', () => { await page.locator('button:has(i.bi-chevron-left)').first().click(); // Wait for the browser URL to update - await expect(page).toHaveURL(/#uri=http:\/\/localhost:3000\/users\.hal\.json/); + expect(await page.evaluate(() => sessionStorage.getItem('hash'))).toContain( + 'uri=http://localhost:3000/users.hal.json' + ); // Wait for navigation to complete await page.waitForLoadState('networkidle'); @@ -247,7 +285,11 @@ test.describe('HAL Explorer App', () => { }); test('should display links and affordances for 401 error with HAL-FORMS content', async ({ page }) => { - await page.goto('/#uri=http://localhost:3000/error-401-with-templates.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/error-401-with-templates.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Verify that error is displayed diff --git a/e2e/ui-blocking.spec.ts b/e2e/ui-blocking.spec.ts index 3e6e1397..ecc38c09 100644 --- a/e2e/ui-blocking.spec.ts +++ b/e2e/ui-blocking.spec.ts @@ -25,7 +25,11 @@ test.describe('UI Blocking During Requests', () => { test('should disable link buttons during request', async ({ page }) => { // Load initial resource - await page.goto('/#uri=http://localhost:3000/index.hal.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/index.hal.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Verify links are visible and enabled @@ -45,7 +49,11 @@ test.describe('UI Blocking During Requests', () => { test('should disable template buttons during request', async ({ page }) => { // Load resource with HAL-FORMS templates - await page.goto('/#uri=http://localhost:3000/movies.hal-forms.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/movies.hal-forms.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Wait for the HAL-FORMS section to be loaded @@ -81,7 +89,11 @@ test.describe('UI Blocking During Requests', () => { test('should disable documentation buttons during request', async ({ page }) => { // Load resource with documentation links - await page.goto('/#uri=http://localhost:3000/index-with-doc-anchor.hal.json'); + await page.goto('/'); + await page.evaluate(() => { + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/index-with-doc-anchor.hal.json'); + window.dispatchEvent(new Event('storage')); + }); await page.waitForLoadState('networkidle'); // Check if documentation button exists and is enabled diff --git a/src/app/app.service.spec.ts b/src/app/app.service.spec.ts index 4d28a113..44dfd808 100644 --- a/src/app/app.service.spec.ts +++ b/src/app/app.service.spec.ts @@ -5,7 +5,7 @@ describe('AppService', () => { let service: AppService; beforeEach(() => { - window.location.hash = ''; + window.sessionStorage.setItem('hash', ''); localStorage.clear(); service = new AppService(); }); @@ -95,8 +95,8 @@ describe('AppService', () => { expect(service.getCustomRequestHeaders()[0].value).toBe('application/json'); expect(service.getCustomRequestHeaders()[1].key).toBe('authorization'); expect(service.getCustomRequestHeaders()[1].value).toBe('bearer euztsfghfhgwztuzt'); - expect(window.location.hash).toBe( - '#hkey0=accept&hval0=application/json&hkey1=authorization&hval1=bearer%20euztsfghfhgwztuzt' + expect(window.sessionStorage.getItem('hash')).toBe( + 'hkey0=accept&hval0=application/json&hkey1=authorization&hval1=bearer euztsfghfhgwztuzt' ); }); @@ -106,7 +106,7 @@ describe('AppService', () => { localStorage.setItem('hal-explorer.columnLayout', '3'); localStorage.setItem('hal-explorer.httpOptions', 'true'); localStorage.setItem('hal-explorer.allHttpMethodsForLinks', 'true'); - window.location.hash = '#hkey0=accept&hval0=text/plain&uri=https://chatty42.herokuapp.com/api/users'; + window.sessionStorage.setItem('hash', 'hkey0=accept&hval0=text/plain&uri=https://chatty42.herokuapp.com/api/users'); service = new AppService(); expect(service.getCustomRequestHeaders()[0].key).toBe('accept'); @@ -121,7 +121,7 @@ describe('AppService', () => { it('should parse window location hash with hval before hkey', () => { localStorage.setItem('hal-explorer.theme', 'Cosmo'); localStorage.setItem('hal-explorer.columnLayout', '3'); - window.location.hash = '#hval0=text/plain&hkey0=accept&uri=https://chatty42.herokuapp.com/api/users'; + window.sessionStorage.setItem('hash', 'hval0=text/plain&hkey0=accept&uri=https://chatty42.herokuapp.com/api/users'); service = new AppService(); expect(service.getCustomRequestHeaders()[0].key).toBe('accept'); @@ -134,7 +134,7 @@ describe('AppService', () => { it('should parse window location hash with deprecated hkey "url"', () => { localStorage.setItem('hal-explorer.theme', 'Cosmo'); localStorage.setItem('hal-explorer.columnLayout', '3'); - window.location.hash = '#hval0=text/plain&hkey0=accept&url=https://chatty42.herokuapp.com/api/users'; + window.sessionStorage.setItem('hash', 'hval0=text/plain&hkey0=accept&url=https://chatty42.herokuapp.com/api/users'); service = new AppService(); expect(service.getCustomRequestHeaders()[0].key).toBe('accept'); @@ -147,7 +147,10 @@ describe('AppService', () => { it('should parse window location hash with unknown hkeys', () => { localStorage.setItem('hal-explorer.theme', 'Cosmo'); localStorage.setItem('hal-explorer.columnLayout', '3'); - window.location.hash = '#xxx=7&hval0=text/plain&hkey0=accept&yyy=xxx&url=https://chatty42.herokuapp.com/api/users'; + window.sessionStorage.setItem( + 'hash', + 'xxx=7&hval0=text/plain&hkey0=accept&yyy=xxx&url=https://chatty42.herokuapp.com/api/users' + ); service = new AppService(); expect(service.getCustomRequestHeaders()[0].key).toBe('accept'); @@ -175,20 +178,20 @@ describe('AppService', () => { }); // Simulate first navigation via browser back button - window.location.hash = '#uri=https://example.com/api/first'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=https://example.com/api/first'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('https://example.com/api/first'); expect(emittedUri).toBe('https://example.com/api/first'); // Simulate second navigation via browser forward button (should work, not skip) - window.location.hash = '#uri=https://example.com/api/second'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=https://example.com/api/second'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('https://example.com/api/second'); expect(emittedUri).toBe('https://example.com/api/second'); // Simulate third navigation (should also work) - window.location.hash = '#uri=https://example.com/api/third'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=https://example.com/api/third'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('https://example.com/api/third'); expect(emittedUri).toBe('https://example.com/api/third'); }); @@ -211,8 +214,8 @@ describe('AppService', () => { expect(service.getUri()).toBe('https://example.com/api/test'); // But the next manual hash change should work - window.location.hash = '#uri=https://example.com/api/manual'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=https://example.com/api/manual'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('https://example.com/api/manual'); expect(emitCount).toBeGreaterThanOrEqual(1); }); @@ -224,8 +227,8 @@ describe('AppService', () => { }); // Start with initial URL (browser navigation) - window.location.hash = '#uri=http://localhost:3000/examples.hal-forms.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/examples.hal-forms.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('http://localhost:3000/examples.hal-forms.json'); expect(emittedUris).toContain('http://localhost:3000/examples.hal-forms.json'); @@ -246,15 +249,15 @@ describe('AppService', () => { const emitCountBeforeBack = emittedUris.length; // First back button - browser changes hash, our code should react - window.location.hash = '#uri=http://localhost:3000/link1.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/link1.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('http://localhost:3000/link1.json'); expect(emittedUris[emittedUris.length - 1]).toBe('http://localhost:3000/link1.json'); expect(emittedUris.length).toBe(emitCountBeforeBack + 1); // Should have emitted // Second back button - browser changes hash, our code should react - window.location.hash = '#uri=http://localhost:3000/examples.hal-forms.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/examples.hal-forms.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.getUri()).toBe('http://localhost:3000/examples.hal-forms.json'); expect(emittedUris[emittedUris.length - 1]).toBe('http://localhost:3000/examples.hal-forms.json'); expect(emittedUris.length).toBe(emitCountBeforeBack + 2); // Should have emitted again @@ -273,8 +276,8 @@ describe('AppService', () => { it('should return true for isFromBrowserNavigation() after browser navigation', () => { // Simulate browser navigation via hash change - window.location.hash = '#uri=http://localhost:3000/test.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/test.json'); + window.dispatchEvent(new HashChangeEvent('storage')); // Should return true because it was browser navigation expect(service.isFromBrowserNavigation()).toBe(true); @@ -299,20 +302,20 @@ describe('AppService', () => { expect(navigationFlags[navigationFlags.length - 1]).toBe(false); // Browser back button (simulated) - window.location.hash = '#uri=http://localhost:3000/page1.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/page1.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(navigationFlags[navigationFlags.length - 1]).toBe(true); // Browser forward button (simulated) - window.location.hash = '#uri=http://localhost:3000/page2.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/page2.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(navigationFlags[navigationFlags.length - 1]).toBe(true); }); it('should reset isFromBrowserNavigation flag after being checked', () => { // Set up browser navigation - window.location.hash = '#uri=http://localhost:3000/test.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/test.json'); + window.dispatchEvent(new HashChangeEvent('storage')); // First check - should be true expect(service.isFromBrowserNavigation()).toBe(true); @@ -326,8 +329,8 @@ describe('AppService', () => { it('should handle multiple browser navigations correctly', () => { // First browser navigation - window.location.hash = '#uri=http://localhost:3000/page1.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/page1.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.isFromBrowserNavigation()).toBe(true); expect(service.isFromBrowserNavigation()).toBe(false); // Reset @@ -336,8 +339,8 @@ describe('AppService', () => { expect(service.isFromBrowserNavigation()).toBe(false); // Second browser navigation - window.location.hash = '#uri=http://localhost:3000/page3.json'; - window.dispatchEvent(new HashChangeEvent('hashchange')); + window.sessionStorage.setItem('hash', 'uri=http://localhost:3000/page3.json'); + window.dispatchEvent(new HashChangeEvent('storage')); expect(service.isFromBrowserNavigation()).toBe(true); expect(service.isFromBrowserNavigation()).toBe(false); // Reset }); diff --git a/src/app/app.service.ts b/src/app/app.service.ts index 4cf7c179..6cc433b3 100644 --- a/src/app/app.service.ts +++ b/src/app/app.service.ts @@ -42,7 +42,7 @@ export class AppService { constructor() { this.initializeFromLocalStorage(); this.handleLocationHash(); - globalThis.addEventListener('hashchange', () => this.handleLocationHash(), false); + window.addEventListener('storage', () => this.handleLocationHash(), false); } private initializeFromLocalStorage(): void { @@ -157,7 +157,7 @@ export class AppService { private parseLocationHashParameters(): RequestHeader[] { const tempHeaders: RequestHeader[] = new Array(5); - const fragment = location.hash.substring(1); + const fragment = window.sessionStorage.getItem('hash') || ''; const regex = /([^&=]+)=([^&]*)/g; let match = regex.exec(fragment); @@ -212,6 +212,6 @@ export class AppService { params.push(`uri=${this.uriParam}`); } - globalThis.location.hash = params.join('&'); + window.sessionStorage.setItem('hash', params.join('&')); } } diff --git a/src/main.ts b/src/main.ts index 9e18a865..3132f926 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,10 +9,27 @@ if (environment.production) { enableProdMode(); } -bootstrapApplication(AppComponent, { - providers: [ - provideZoneChangeDetection(), - importProvidersFrom(BrowserModule, FormsModule), - provideHttpClient(withInterceptorsFromDi()), - ], -}).catch(err => console.log(err)); +let bootstrapped = false; +function bootstrap() { + bootstrapped = true; + bootstrapApplication(AppComponent, { + providers: [ + provideZoneChangeDetection(), + importProvidersFrom(BrowserModule, FormsModule), + provideHttpClient(withInterceptorsFromDi()), + ], + }).catch(err => console.log(err)); +} + +if (window.opener) { + window.addEventListener('message', event => { + window.sessionStorage.setItem('hash', event.data); + window.dispatchEvent(new Event('storage')); + if (!bootstrapped) { + bootstrap(); + } + }); + window.opener.postMessage('ready'); +} else { + bootstrap(); +}