1

Tech stack: Angular v15 and Cypress v12.

My example component that I'm testing:

import { Component } from '@angular/core';
import { UserHttp } from '../../services';

@Component({
  selector: 'example-view',
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  constructor(
    private userHttp: UserHttp,
  ) { }
}

My example component test

import { HttpClientModule } from '@angular/common/http';

import { ExampleComponent } from './example.component';
import { UserHttp } from '../../services';

describe('Example component', () => {
  beforeEach(() => {
    cy.mount(ExampleComponent, {
      providers: [UserHttp],
      imports: [HttpClientModule]
    });
  });

  it('should display default title', () => {
    cy.get('h2').should('exist');
  });
});

My UserHttp service that I'm injecting:

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

@Injectable()
export class UserHttp {

  test(): Observable<any> {
    return of({});
  }
}

Current status:

The above test will fail if I leave the import as: import { Http } from '..';
But if I change it to this it works: import { Http } from './http';

The reason I have it as import { Http } from '..' is because I use an index file to export all the services like this:

// index.ts: services:
import { UserHttp } from './http/user.http';
import { StorageService } from './storage.service';

export * from './http/user.http';
export * from './storage.service';

export const SERVICES = [
  StorageService,
  UserHttp,
];

My StorageService:

import { Injectable } from '@angular/core';

import { environment } from '../../environments/environment';

@Injectable()
export class StorageService {
  baseKey: string = environment.baseStorageKey;

  constructor() {}

  setLocalStorage(key: string, value: any): void {
    this.removeLocalStorage(key);
    localStorage.setItem(`${this.baseKey}${key}`, JSON.stringify(value));
  }

  getLocalStorage(key: string): any {
    const item = localStorage.getItem(`${this.baseKey}${key}`);
    return item !== null ? JSON.parse(item) : '';
  }

  removeLocalStorage(key: string): void {
    localStorage.removeItem(`${this.baseKey}${key}`);
  }

  removeBatchLocalStorage(keys: string[]): void {
    keys.forEach((key: string) => {
      localStorage.removeItem(`${this.baseKey}${key}`);
    });
  }
}

This is my cypress config:

import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
  chromeWebSecurity: false,
  screenshotsFolder: "cypress/snapshots",
  trashAssetsBeforeRuns: true,
  viewportWidth: 1400,
  viewportHeight: 1200,
  video: false,
  env: {
    local: "http://localhost:4200/",
    staging: "https://hidden.co.uk/",
    user: {
      email: "hidden",
      password: "hidden",
    },
  },
  component: {
    devServer: {
      framework: "angular",
      bundler: "webpack",
    },
    specPattern: "**/*.cy.ts",
  },
});

Is there anyway to set index.ts file approach to work in the tsconfig or cypress config?

Current Error I get:

     TypeError
The following error originated from your test code, not from Cypress.

  > Cannot read properties of undefined (reading 'StorageService')

When Cypress detects uncaught errors originating from your test code it will automatically fail the current test.

Cypress could not associate this error to any specific test.

We dynamically generated a new test to display this failure.

My TS Config file:

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": false,
    "strictPropertyInitialization": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2020",
    "module": "es2020",
    "skipLibCheck": true,
    "allowJs": true,
    "types": [
      "node"
    ],
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "@app/*": ["src/app/*"],
      "@services/*": ["src/app/services/*"]
    }
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": false,
    "strictInputAccessModifiers": false,
    "strictTemplates": false
  }
}

My http.ts server that is extended by the userHttp service:

import { HttpClient, HttpParams, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

import { environment } from '../../../environments/environment';

@Injectable()
export abstract class Http {
  protected baseUrl = environment.baseUrl;
  protected headers: HttpHeaders;

  constructor(public httpClient: HttpClient) {}

  protected get<T>(path: string, options: any = {}, noBaseUrl = false): Observable<T> {
    const url: string = this.createUrlString(path, noBaseUrl);
    const params: HttpParams = this.getHttpParams(options.queryString);
    return this.httpClient.get<T>(url, { params });
  }

  protected post<T>(path: string, data = {}, noBaseUrl = false): Observable<T> {
    const url: string = this.createUrlString(path, noBaseUrl);
    const options = { headers: this.headers };
    return this.httpClient.post<T>(url, { ...data, lang: 'uk' }, options);
  }

  protected createUrlString(resourcePath: string, noBaseUrl: boolean): string {
    return noBaseUrl ? `${resourcePath}` : `${this.baseUrl}${resourcePath}`;
  }

  protected getHttpParams(params: any): HttpParams {
    let httpParams: HttpParams = new HttpParams();

    if (params) {
      for (const prop in params) {
        if (params.hasOwnProperty(prop)) {
          const parameterValue: string = params[prop].toString();
          httpParams = httpParams.append(prop, parameterValue);
        }
      }
    }
    return httpParams;
  }
}

Gui error message

enter image description here

AngularM
  • 15,982
  • 28
  • 94
  • 169

2 Answers2

5

If I've reproduced the error correctly (it seems so because the change to the import you mentioned fixes it), then the error is

ReferenceError
The following error originated from your test code, not from Cypress.

Cannot access 'UserHttp' before initialization

You can fix it in the component by importing UserHttp via the services index rather than directly from it's implementation.

example component

import { Component } from '@angular/core';
// import { UserHttp } from '../../services/http/user.http';
import { UserHttp } from '../../services';

@Component({
  selector: 'example-view',
  templateUrl: './example.component.html',
})
export class ExampleComponent {
  constructor(
    private userHttp: UserHttp,
  ) { }
}

enter image description here

The reason, I think, is that UserHttp is dependent on services/index but this index is not directly referenced by the test, and therefore does not get complied.

The error message does seem confusing, it should be complaining about Http.ts rather than UserHttp.ts.

  • Hi, I've updated the error I get above for your solution. I get an ivy error – AngularM Jun 06 '23 at 12:26
  • My error occurs because I'm importing using this: import { UserHttp } from '../../services' in all the files. The reason I do this is because I use an index.ts file in my services folder. And in the index.ts file I import and export like so: export * from './http/http'; export * from './http/user.http'; – AngularM Jun 06 '23 at 12:31
  • I've just added my tsconfig above because the ivy error mentions this: is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property. – AngularM Jun 06 '23 at 12:35
  • 1
    I was originally running Angular 14 - just managed to upgrade to Angular 15.2.0 but it's still passing the test after upgrade. I also pasted your tsconfig.json, and it's still working. – SuchAnIgnorantThingToDo-UKR Jun 06 '23 at 13:03
  • The only other differences are `ResponseModel` (changed to `any` type) and `Http.ts` is as simple as needed to get things working. Maybe the base class of `Http` is the problem, as I don't know what that is. – SuchAnIgnorantThingToDo-UKR Jun 06 '23 at 13:07
  • This is the script I run: "cy:gui": "cypress open -e \"TAGS=not @ignore\"", npm run cy:gui – AngularM Jun 06 '23 at 13:46
  • I've also just added my Http abstract service that my service implements – AngularM Jun 06 '23 at 13:48
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/253969/discussion-between-angularm-and-suchanignorantthingtodo-ukr). – AngularM Jun 06 '23 at 14:05
  • I've added the error message above and the latest code I have – AngularM Jun 06 '23 at 14:33
0

I found the issue is with the typescript config settings.

I found this solution by making a new angular version 15 app with just cypress and it worked.

So I then compared the two tsconfig.json files.

This is the working tsconfig.json file that solved this issue:

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "forceConsistentCasingInFileNames": true,
    "strict": false,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2017",
    "module": "es2020",
    "allowJs": true,
    "lib": [
      "es2020",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "enableI18nLegacyMessageIdFormat": false,
    "strictInjectionParameters": false,
    "strictInputAccessModifiers": false,
    "strictTemplates": false
  }
}
AngularM
  • 15,982
  • 28
  • 94
  • 169