Skip to content

Commit

Permalink
Use show requests for hasMany associations (#2173)
Browse files Browse the repository at this point in the history
Using show requests for hasMany associations allows the http debounce
interceptor to cache hasMany association requests

Fixes: #2160
  • Loading branch information
hudson-newey authored Nov 25, 2024
1 parent 3ad116b commit 96ce19c
Show file tree
Hide file tree
Showing 12 changed files with 603 additions and 491 deletions.
39 changes: 23 additions & 16 deletions src/app/components/admin/orphan/details/details.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,23 @@ import { SharedModule } from "@shared/shared.module";
import { generateBawApiError } from "@test/fakes/BawApiError";
import { generateSite } from "@test/fakes/Site";
import { assertDetail, Detail } from "@test/helpers/detail-view";
import { nStepObservable } from "@test/helpers/general";
import { interceptMappedApiRequests, nStepObservable } from "@test/helpers/general";
import { assertPageInfo } from "@test/helpers/pageRoute";
import { mockActivatedRoute } from "@test/helpers/testbed";
import { Subject } from "rxjs";
import { of, Subject } from "rxjs";
import { appLibraryImports } from "src/app/app.module";
import { AssociationInjector } from "@models/ImplementsInjector";
import { ASSOCIATION_INJECTOR } from "@services/association-injector/association-injector.tokens";
import { Id } from "@interfaces/apiInterfaces";
import { AdminOrphanComponent } from "./details.component";

describe("AdminOrphanComponent", () => {
let component: AdminOrphanComponent;
let fixture: ComponentFixture<AdminOrphanComponent>;
let injector: AssociationInjector;
const siteProjectIds = [1, 2, 3];

function configureTestingModule(model: Site, error?: BawApiError) {
function setup(model: Site, error?: BawApiError) {
TestBed.configureTestingModule({
imports: [
...appLibraryImports,
Expand All @@ -50,31 +52,36 @@ describe("AdminOrphanComponent", () => {

fixture = TestBed.createComponent(AdminOrphanComponent);
injector = TestBed.inject(ASSOCIATION_INJECTOR);

const accountsApi = TestBed.inject(
ACCOUNT.token
) as SpyObject<AccountsService>;
const projectsApi = TestBed.inject(
PROJECT.token
) as SpyObject<ProjectsService>;

component = fixture.componentInstance;

const mockProjectApiResponses = new Map<any, Project>([
[1, new Project({ id: 1, siteIds: [1], name: "custom project" })],
[2, new Project({ id: 2, siteIds: [1], name: "custom project" })],
[3, new Project({ id: 3, siteIds: [1], name: "custom project" })],
]);
projectsApi.show.and.callFake((id: Id) =>
of(mockProjectApiResponses.get(id))
);

const accountsSubject = new Subject<User>();
const projectsSubject = new Subject<Project[]>();
const promise = Promise.all([
nStepObservable(
accountsSubject,
() => new User({ id: 1, userName: "custom username" })
),
nStepObservable(projectsSubject, () =>
[1, 2, 3].map(
(id) => new Project({ id, siteIds: [1], name: "custom project" })
)
),
...interceptMappedApiRequests(projectsApi.show, mockProjectApiResponses),
]);

// Catch associated models
accountsApi.show.and.callFake(() => accountsSubject);
projectsApi.filter.and.callFake(() => projectsSubject);

// Update model to contain injector
if (model) {
Expand All @@ -87,28 +94,28 @@ describe("AdminOrphanComponent", () => {
assertPageInfo<Site>(AdminOrphanComponent, "Test Orphaned Site", {
site: {
model: new Site(generateSite({ name: "Test Orphaned Site" })),
}
},
});

it("should create", () => {
configureTestingModule(new Site(generateSite()));
setup(new Site(generateSite()));
fixture.detectChanges();
expect(component).toBeTruthy();
});

it("should handle error", () => {
configureTestingModule(undefined, generateBawApiError());
setup(undefined, generateBawApiError());
fixture.detectChanges();
expect(component).toBeTruthy();
});

describe("details", () => {
const model = new Site(
generateSite({ locationObfuscated: true, projectIds: [1, 2, 3] })
generateSite({ locationObfuscated: true, projectIds: siteProjectIds })
);

beforeEach(async function () {
const promise = configureTestingModule(model);
const promise = setup(model);
fixture.detectChanges();
await promise;
fixture.detectChanges();
Expand Down Expand Up @@ -150,7 +157,7 @@ describe("AdminOrphanComponent", () => {
{
label: "Projects",
key: "projects",
children: [1, 2, 3].map((id) => ({
children: siteProjectIds.map((id) => ({
model: `Project: custom project (${id})`,
})),
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { AudioRecording } from "@models/AudioRecording";
import { generateAudioRecording } from "@test/fakes/AudioRecording";
import { AssociationInjector } from "@models/ImplementsInjector";
import { ASSOCIATION_INJECTOR } from "@services/association-injector/association-injector.tokens";
import { Id } from "@interfaces/apiInterfaces";
import { AnnotationSearchFormComponent } from "./annotation-search-form.component";

describe("AnnotationSearchFormComponent", () => {
Expand Down Expand Up @@ -61,22 +62,30 @@ describe("AnnotationSearchFormComponent", () => {
sitesApiSpy = spectator.inject(SHALLOW_SITE.token);
recordingsApiSpy = spectator.inject(AUDIO_RECORDING.token);

mockTagsResponse = Array.from(
{ length: 10 },
() => new Tag(generateTag(), injector)
);
mockSitesResponse = Array.from(
{ length: 10 },
() => new Site(generateSite(), injector)
);
mockProject = new Project(generateProject(), injector);
mockRecording = new AudioRecording(generateAudioRecording(), injector);
// so that the models can use their associations, we need to provide the
// association injector to the mock models
mockTagsResponse.forEach((tag) => (tag["injector"] = injector));
mockSitesResponse.forEach((site) => (site["injector"] = injector));
mockProject["injector"] = injector;
mockRecording["injector"] = injector

modelChangeSpy = spyOn(spectator.component.searchParametersChange, "emit");

// we mock both filter and show requests because we need to have consistent
// mock data for the typeahead queries that use filter requests, and the
// has-many associations that use show requests
tagsApiSpy.filter.andCallFake(() => of(mockTagsResponse));
tagsApiSpy.show.andCallFake((id: Id) =>
of(mockTagsResponse.find((tag) => tag.id === id))
);

sitesApiSpy.filter.andCallFake(() => of(mockSitesResponse));
sitesApiSpy.show.andCallFake((id: Id) =>
of(mockSitesResponse.find((site) => site.id === id))
);

recordingsApiSpy.filter.andCallFake(() => of([mockRecording]));
recordingsApiSpy.show.andCallFake(() => of(mockRecording));

const searchParameters = new AnnotationSearchParameters(params, injector);
searchParameters.routeProjectModel = mockProject;
Expand Down Expand Up @@ -104,6 +113,19 @@ describe("AnnotationSearchFormComponent", () => {
spectator.query(".advanced-filters>[ng-reflect-collapsed]");
const recordingsTypeahead = () => spectator.query("#recordings-input");

beforeEach(() => {
mockTagsResponse = Array.from(
{ length: 10 },
() => new Tag(generateTag())
);
mockSitesResponse = Array.from(
{ length: 10 },
() => new Site(generateSite())
);
mockProject = new Project(generateProject());
mockRecording = new AudioRecording(generateAudioRecording());
});

it("should create", () => {
setup();
expect(spectator.component).toBeInstanceOf(AnnotationSearchFormComponent);
Expand All @@ -125,9 +147,11 @@ describe("AnnotationSearchFormComponent", () => {

// check the population of a typeahead input that does not use a property backing
it("should pre-populate the tags typeahead input if provided in the search parameters model", () => {
setup({ tags: "0" });
const testedTag = mockTagsResponse[0];

setup({ tags: testedTag.id.toString() });
const realizedTagPills = tagPills();
expect(realizedTagPills[0].innerText).toEqual(`${mockTagsResponse[0]}`);
expect(realizedTagPills[0].innerText).toEqual(`${testedTag.text}`);
});

// check the population of an external component that is not a typeahead input
Expand Down
Loading

0 comments on commit 96ce19c

Please sign in to comment.