Skip to content

Commit

Permalink
fix(product-composite): fix and extract query param (#2437)
Browse files Browse the repository at this point in the history
The `DaffProductPageLoadSuccess` runs at
different times in the action stream depending on server side render
or not. This meant that, during SSR, the `composite_configuration` was
feature was never used correctly as the Router state is only set AFTER
the `DaffProductPageLoadSuccess`.

Additionally, previously the query param service retrieval was inside
of the effect that handled the query param. This made testing the effect
difficult so I extracted it.
  • Loading branch information
damienwebdev committed May 12, 2023
1 parent 37e0548 commit 98d5272
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { Component } from '@angular/core';
import {
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import {
hot,
cold,
} from 'jasmine-marbles';
import { Observable } from 'rxjs';

import { DaffBase64ServiceToken } from '@daffodil/core';
import {
DaffCompositeProduct,
DaffProductCompositeSelectionPayload,
Expand All @@ -23,6 +16,7 @@ import { DaffCompositeProductFactory } from '@daffodil/product-composite/testing
import { DaffProductPageLoadSuccess } from '@daffodil/product/state';

import { daffProductCompositeRoutingProvideConfig } from '../config/public_api';
import { DaffProductCompositeQueryParamService } from '../services/query-param.service';
import { DaffProductCompositePageEffects } from './product-page.effects';

@Component({ template: '' })
Expand All @@ -31,36 +25,33 @@ class TestComponent {}
describe('@daffodil/product-composite/routing | DaffProductCompositePageEffects', () => {
let actions$: Observable<any>;
let effects: DaffProductCompositePageEffects;
let router: Router;
let compositeProductFactory: DaffCompositeProductFactory;

let selection: DaffProductCompositeSelectionPayload;
let queryParam: string;
let product: DaffCompositeProduct;
let param: string;

beforeEach(() => {
queryParam = 'queryParam';

TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{
path: '**',
component: TestComponent,
},
]),
],
providers: [
DaffProductCompositePageEffects,
provideMockActions(() => actions$),
daffProductCompositeRoutingProvideConfig({
compositeSelectionQueryParam: queryParam,
}),
{
provide: DaffProductCompositeQueryParamService,
useValue: {
get: () => param,
},
},
],
});

effects = TestBed.inject(DaffProductCompositePageEffects);
router = TestBed.inject(Router);
compositeProductFactory = TestBed.inject(DaffCompositeProductFactory);

product = compositeProductFactory.create();
Expand All @@ -78,95 +69,93 @@ describe('@daffodil/product-composite/routing | DaffProductCompositePageEffects'
let expected;

describe('when called with a route with a set query param', () => {
beforeEach(fakeAsync(() => {
beforeEach(() => {
const response = {
id: product.id,
products: [product],
};
const productLoadSuccessAction = new DaffProductPageLoadSuccess(response);
router.navigateByUrl(`/testpath?${queryParam}=${encodeURIComponent(btoa(JSON.stringify(selection)))}`);
tick();
param = `${btoa(JSON.stringify(selection))}`;

actions$ = hot('--a', { a: productLoadSuccessAction });
expected = cold('--(abc)', {
a: new DaffCompositeProductApplyOption(product.id, product.items[0].id, product.items[0].options[0].id, product.items[0].options[0].quantity),
b: new DaffCompositeProductApplyOption(product.id, product.items[0].id, product.items[0].options[1].id, product.items[0].options[1].quantity),
c: new DaffCompositeProductApplyOption(product.id, product.items[1].id, product.items[1].options[0].id, product.items[1].options[0].quantity),
});
}));
});

it('should apply the composite product options specified', () => {
expect(effects.preselectCompositeOptions$).toBeObservable(expected);
});
});

describe('when called with a route with no set query param', () => {
beforeEach(fakeAsync(() => {
router.navigateByUrl(`/testpath?some_other_query_param=${encodeURIComponent(queryParam)}`);
tick();
beforeEach(() => {
param = ``;
const response = {
id: product.id,
products: [product],
};
const productLoadSuccessAction = new DaffProductPageLoadSuccess(response);
actions$ = hot('--a', { a: productLoadSuccessAction });
expected = cold('---');
}));
});

it('should not apply any composite product options', () => {
expect(effects.preselectCompositeOptions$).toBeObservable(expected);
});
});

describe('when called with a route with junk set as the query param', () => {
beforeEach(fakeAsync(() => {
router.navigateByUrl(`/testpath?${queryParam}=iamjunkanddonotdecodetoanythingworthwhile`);
tick();
beforeEach(() => {
param = `iamjunkanddonotdecodetoanythingworthwhile`;
const response = {
id: product.id,
products: [product],
};
const productLoadSuccessAction = new DaffProductPageLoadSuccess(response);
actions$ = hot('--a', { a: productLoadSuccessAction });
expected = cold('---');
}));
});

it('should not error or apply any composite product options', () => {
expect(effects.preselectCompositeOptions$).toBeObservable(expected);
});
});

describe('when called with a route with nothing set as the query param', () => {
beforeEach(fakeAsync(() => {
router.navigateByUrl(`/testpath?${queryParam}=`);
tick();
beforeEach(() => {
param = ``;

const response = {
id: product.id,
products: [product],
};
const productLoadSuccessAction = new DaffProductPageLoadSuccess(response);
actions$ = hot('--a', { a: productLoadSuccessAction });
expected = cold('---');
}));
});

it('should not error or apply any composite product options', () => {
expect(effects.preselectCompositeOptions$).toBeObservable(expected);
});
});

describe('when called with a route with an invalid selection set as the query param', () => {
beforeEach(fakeAsync(() => {
router.navigateByUrl(`/testpath?${queryParam}=${encodeURIComponent(btoa(JSON.stringify({
beforeEach(() => {
param = `${btoa(JSON.stringify({
somerandomid: ['iamnotanoptionid'],
})))}`);
tick();
}))}`;

const response = {
id: product.id,
products: [product],
};
const productLoadSuccessAction = new DaffProductPageLoadSuccess(response);
actions$ = hot('--a', { a: productLoadSuccessAction });
expected = cold('---');
}));
});

it('should not error or apply any composite product options', () => {
expect(effects.preselectCompositeOptions$).toBeObservable(expected);
Expand Down
18 changes: 8 additions & 10 deletions libs/product-composite/routing/src/effects/product-page.effects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Location } from '@angular/common';
import {
DOCUMENT,
Location,
} from '@angular/common';
import {
Inject,
Injectable,
Expand Down Expand Up @@ -40,6 +43,7 @@ import {
DaffProductCompositeRoutingConfig,
DAFF_PRODUCT_COMPOSITE_ROUTING_CONFIG,
} from '../config/public_api';
import { DaffProductCompositeQueryParamService } from '../services/query-param.service';

/**
* Builds the apply actions from the list of selected options.
Expand All @@ -64,24 +68,18 @@ function buildApplyActions<T extends DaffCompositeProduct = DaffCompositeProduct
export class DaffProductCompositePageEffects<T extends DaffCompositeProduct = DaffCompositeProduct> {
constructor(
private actions$: Actions,
private route: ActivatedRoute,
private paramGetter: DaffProductCompositeQueryParamService,
@Inject(DAFF_PRODUCT_COMPOSITE_ROUTING_CONFIG) private config: DaffProductCompositeRoutingConfig,
) {}

/**
* Get the value of the configured composite selection query param.
*/
private getQueryParam(): string {
return this.route.snapshot.queryParamMap.get(this.config.compositeSelectionQueryParam);
}

/**
* Applies composite item options based on the value of the configured query param.
*/
preselectCompositeOptions$: Observable<typeof EMPTY | DaffCompositeProductApplyOption<T>> = createEffect(() => this.actions$.pipe(
ofType(DaffProductPageActionTypes.ProductPageLoadSuccessAction),
switchMap((action: DaffProductPageLoadSuccess<T>) => {
const queryParam = this.getQueryParam();
const queryParam = this.paramGetter.get();

// get the product corresponding to the current product page
const product: DaffCompositeProduct = action.payload.products.filter(({ id }) => id === action.payload.id)[0];

Expand Down
4 changes: 4 additions & 0 deletions libs/product-composite/routing/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ import {
DAFF_PRODUCT_COMPOSITE_ROUTING_CONFIG,
} from './config/public_api';
import { DaffProductCompositePageEffects } from './effects/public_api';
import { DaffProductCompositeQueryParamService } from './services/query-param.service';

@NgModule({
imports: [
EffectsModule.forFeature([DaffProductCompositePageEffects]),
],
providers: [
DaffProductCompositeQueryParamService,
],
})
export class DaffProductCompositeRoutingModule {
static withConfig(config?: DaffProductCompositeRoutingConfig): ModuleWithProviders<DaffProductCompositeRoutingModule> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TestBed } from '@angular/core/testing';

import { DaffProductCompositeQueryParamService } from './query-param.service';

describe('@daffodil/product-composite/routing | DaffProductCompositeQueryParamService', () => {
let service: DaffProductCompositeQueryParamService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
],
providers: [
DaffProductCompositeQueryParamService,
],
});

service = TestBed.inject(DaffProductCompositeQueryParamService);
});

it('should create', () => {
expect(service).toBeTruthy();
});
});
28 changes: 28 additions & 0 deletions libs/product-composite/routing/src/services/query-param.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { DOCUMENT } from '@angular/common';
import {
Inject,
Injectable,
} from '@angular/core';

import {
DAFF_PRODUCT_COMPOSITE_ROUTING_CONFIG,
DaffProductCompositeRoutingConfig,
} from '../config/public_api';

@Injectable()
export class DaffProductCompositeQueryParamService {

constructor(
@Inject(DOCUMENT) private document: any,
@Inject(DAFF_PRODUCT_COMPOSITE_ROUTING_CONFIG) private config: DaffProductCompositeRoutingConfig) {
}

/**
* Get the value of the configured composite selection query param.
*/
public get(): string {
return (
new URL((<any>this.document).location.toString())
).searchParams.get(this.config.compositeSelectionQueryParam);
}
}

0 comments on commit 98d5272

Please sign in to comment.