Skip to content
Open
66 changes: 54 additions & 12 deletions e2e/hal-explorer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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');
Expand All @@ -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
Expand Down
18 changes: 15 additions & 3 deletions e2e/ui-blocking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
69 changes: 36 additions & 33 deletions src/app/app.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('AppService', () => {
let service: AppService;

beforeEach(() => {
window.location.hash = '';
window.sessionStorage.setItem('hash', '');
localStorage.clear();
service = new AppService();
});
Expand Down Expand Up @@ -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'
);
});

Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -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');
});
Expand All @@ -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);
});
Expand All @@ -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');

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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

Expand All @@ -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
});
Expand Down
6 changes: 3 additions & 3 deletions src/app/app.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -212,6 +212,6 @@ export class AppService {
params.push(`uri=${this.uriParam}`);
}

globalThis.location.hash = params.join('&');
window.sessionStorage.setItem('hash', params.join('&'));
}
}
Loading