Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Legacy pathway links in the URL fragment.
*
* The old browser addressed a pathway in the fragment, and this site's own news
* archive is full of those links: 770 spelled `#/R-HSA-1430728` and 86 spelled
* `#R-HSA-202733`, with no slash. The pattern used to require the slash, so the
* second kind matched nothing and opened the browser with no pathway in it.
*
* The cases below are taken from what is actually in the content, not invented.
*/
import { describe, expect, it } from 'vitest';
import { FRAGMENT_PATTERN } from './url-state.service';

/** What the subscriber does with a fragment, reduced to its decisions. */
function route(fragment: string) {
const match = fragment.match(FRAGMENT_PATTERN);
if (!match?.groups) return { id: undefined, params: {} as Record<string, unknown> };
const params: Record<string, unknown> = {};
if (match.groups['params']) {
for (const [key, value] of match.groups['params'].split('&').map((p) => p.split('='))) {
params[key] = value || true;
}
}
return { id: match.groups['id'], params };
}

describe('a legacy pathway link in the fragment', () => {
it('opens the pathway when the link carries a slash', () => {
expect(route('/R-HSA-1430728').id).toBe('R-HSA-1430728');
});

it('opens the pathway when the link carries no slash', () => {
// 41 links in the news archive look like this, and every one of them used
// to land on an empty browser.
expect(route('R-HSA-202733').id).toBe('R-HSA-202733');
expect(route('R-HSA-913531').id).toBe('R-HSA-913531');
});

it('drops a stIdVersion rather than passing it on as a parameter', () => {
// 45 links carry one. It used to arrive as a query parameter named ".1".
for (const fragment of ['R-HSA-8853659.1', '/R-HSA-69231.4', 'R-HSA-3371497.12']) {
const { id, params } = route(fragment);
expect(id, fragment).toMatch(/^R-[A-Z]{3}-\d+$/);
expect(Object.keys(params), fragment).toEqual([]);
}
});

it('keeps the parameters an old link carries', () => {
const { id, params } = route('/R-HSA-8876384&PATH=R-HSA-1643685,R-HSA-5663205');
expect(id).toBe('R-HSA-8876384');
expect(params).toEqual({ PATH: 'R-HSA-1643685,R-HSA-5663205' });
});

it('leaves a fragment that is not a pathway alone', () => {
// A section to scroll to, and the old analysis-tool fragment. Matching
// these would turn them into a route to nowhere and a junk query
// parameter; they have to fall through untouched.
for (const fragment of ['introduction', 'summation', 'TOOL=AT', 'literature']) {
expect(route(fragment).id, fragment).toBeUndefined();
expect(Object.keys(route(fragment).params), fragment).toEqual([]);
}
});

it('does not take an identifier out of the middle of something else', () => {
for (const fragment of ['see-R-HSA-202733-here', 'XR-HSA-202733']) {
expect(route(fragment).id, fragment).toBeUndefined();
}
});
});
19 changes: 18 additions & 1 deletion projects/pathway-browser/src/app/services/url-state.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,24 @@ import type { Analysis } from '../model/analysis.model';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { toSignal } from '@angular/core/rxjs-interop';

const FRAGMENT_PATTERN = /\/(?<id>R-[A-Z]{3}-\d+)?&?(?<params>.*)/;
/**
* A legacy pathway link, as the old browser addressed one in the URL fragment.
*
* Both spellings are in the wild, because the old site produced both: 770 links
* in this site's own news archive say `#/R-HSA-1430728` and 86 say
* `#R-HSA-202733` with no slash. Requiring the slash meant the second kind
* matched nothing, so every one of them opened the browser with no pathway in
* it -- a blank page from a link in a release announcement.
*
* The id must be there for this to match at all. A fragment that is not a
* pathway reference -- `#introduction`, naming a section to scroll to -- has to
* fall through untouched rather than be read as a stale route.
*
* A trailing `.4` is a stIdVersion. The old links carry it, the content service
* does not want it, and it was previously parsed as a query parameter called
* ".4"; it is consumed and dropped here.
*/
export const FRAGMENT_PATTERN = /^\/?(?<id>R-[A-Z]{3}-\d+)(?:\.\d+)?(?:&(?<params>.*))?$/;

export type UrlParam<T> = WritableSignal<T> & {
otherTokens?: string[];
Expand Down
Loading