diff --git a/CHANGELOG.md b/CHANGELOG.md index fe25afa..11c6f25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- [#14](https://github.com/NoelDeMartin/umai/issues/14) "Back to cookbook" navigates to authorization page. + ## [v0.1.5](https://github.com/NoelDeMartin/umai/releases/tag/v0.1.5) - 2023-12-22 ### Changed diff --git a/cypress/integration/github.spec.ts b/cypress/integration/github.spec.ts index a9bfd0b..ce49256 100644 --- a/cypress/integration/github.spec.ts +++ b/cypress/integration/github.spec.ts @@ -1,3 +1,5 @@ +import { cssPodUrl } from '@cy/support/commands/auth'; + describe('Github issues', () => { beforeEach(() => { @@ -22,4 +24,29 @@ describe('Github issues', () => { cy.see('Change recipe image'); }); + it('#14 "Back to cookbook" navigates to authorization page', () => { + // Arrange + cy.visit('/?authenticator=inrupt'); + cy.startApp(); + cy.press('Connect your Solid POD'); + cy.ariaInput('Login url').clear().type(cssPodUrl('/alice/{enter}')); + cy.cssAuthorize({ + reset: { + typeIndex: true, + cookbook: true, + recipe: 'ramen', + }, + }); + cy.waitForReload({ resetProfiles: true }); + cy.press('Ramen'); + cy.see('Broth'); + cy.reload(); + + // Act + cy.press('back to cookbook'); + + // Assert + cy.url().should('equal', `${Cypress.config('baseUrl')}/`); + }); + }); diff --git a/src/framework/routing/router/FrameworkRouter.ts b/src/framework/routing/router/FrameworkRouter.ts index 2e8e7b7..2218a14 100644 --- a/src/framework/routing/router/FrameworkRouter.ts +++ b/src/framework/routing/router/FrameworkRouter.ts @@ -1,5 +1,11 @@ -import { Storage, once, toString } from '@noeldemartin/utils'; -import type { RouteLocationNormalized, RouteLocationRaw, Router } from 'vue-router'; +import { Storage, fail, once, toString } from '@noeldemartin/utils'; +import type { + NavigationFailure, + RouteLocationNormalized, + RouteLocationNormalizedLoaded, + RouteLocationRaw, + Router, +} from 'vue-router'; import type { SolidModel } from 'soukai-solid'; const HISTORY_LENGTH = 10; @@ -11,16 +17,36 @@ interface ModelBindingConfiguration { export default class FrameworkRouter { - private modelBindings: Record = {}; - private runtimeHistory: RouteLocationNormalized[] = []; + private _modelBindings?: Record; + private _runtimeHistory?: RouteLocationNormalized[]; + private _replacements?: WeakMap; + private _originalReplace?: Router['replace']; public get previousRoute(): RouteLocationNormalized | null { - return this.runtimeHistory[1] ?? null; + return this.runtimeHistory?.[1] ?? null; } - public initialize(this: Router): void { - this.modelBindings = {}; - this.runtimeHistory = []; + private get modelBindings(): Record { + return this._modelBindings ?? fail('modelBindings not initialized in framework router'); + } + + private get runtimeHistory(): RouteLocationNormalized[] { + return this._runtimeHistory ?? fail('runtimeHistory not initialized in framework router'); + } + + private get replacements(): WeakMap { + return this._replacements ?? fail('replacements not initialized in framework router'); + } + + private get originalReplace(): Router['replace'] { + return this._originalReplace ?? fail('originalReplace not initialized in framework router'); + } + + public initialize(this: Router, originalReplace: Router['replace']): void { + this._modelBindings = {}; + this._runtimeHistory = []; + this._replacements = new WeakMap(); + this._originalReplace = originalReplace; this.beforeEach(once(() => this.handleGithubPagesRedirect())); this.beforeEach(route => this.onCurrentRouteChanged(route)); } @@ -57,7 +83,22 @@ export default class FrameworkRouter { return name.test(toString(this.currentRoute.value.name)); } + public async replace(this: Router, to: RouteLocationRaw): Promise { + this.replacements.set(this.currentRoute.value, to); + + return this.originalReplace.call(this, to); + } + private onCurrentRouteChanged(route: RouteLocationNormalized): void { + const lastRoute = this.runtimeHistory[0]; + + if (lastRoute && this.replacements.get(lastRoute)) { + this.runtimeHistory[0] = route; + this.replacements.delete(lastRoute); + + return; + } + this.runtimeHistory.unshift(route); this.runtimeHistory.splice(HISTORY_LENGTH); } diff --git a/src/framework/routing/router/index.ts b/src/framework/routing/router/index.ts index bf7d78d..ec24f66 100644 --- a/src/framework/routing/router/index.ts +++ b/src/framework/routing/router/index.ts @@ -1,8 +1,7 @@ -import { applyMixins, fail, tap } from '@noeldemartin/utils'; +import { fail, objectWithout, tap } from '@noeldemartin/utils'; import { createRouter, onBeforeRouteLeave, onBeforeRouteUpdate, useRoute } from 'vue-router'; import { computed, defineComponent, h, onUnmounted, ref, watch } from 'vue'; import type { Component, ConcreteComponent } from 'vue'; -import type { Constructor } from '@noeldemartin/utils'; import type { RouteLocationNormalizedLoaded, RouteLocationRaw, @@ -53,9 +52,17 @@ function enhanceRoute(route: AppRoute): AppRoute { } function enhanceRouter(router: VueRouter): VueRouter { - applyMixins(router.constructor as Constructor, [FrameworkRouter]); + const originalReplace = router.replace; + const mixinDescriptors = objectWithout( + Object.getOwnPropertyDescriptors(FrameworkRouter.prototype), + ['constructor'], + ); + + for (const [propertyName, propertyDescriptor] of Object.entries(mixinDescriptors)) { + Object.defineProperty(router, propertyName, propertyDescriptor); + } - return tap(router, router => router.initialize()); + return tap(router, router => router.initialize(originalReplace)); } function enhancedRouteComponentSetup(pageComponent: ConcreteComponent, meta?: AppRouteMeta) {