Angular2 Testing http service saying map function not supported - testing

I trying to create a test against a simple service, but I am getting an error that says "TypeError: Object doesn't support property or method 'map'" When I run this service for real (not as a test) it works fine and I don't have any issues. This is the first time I'm trying to get a test setup for Angular2, so I could be missing something. Here are my components.
recentActivity.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import * as toPromise from 'rxjs/add/operator/toPromise';
import * as map from 'rxjs/add/operator/map';
import { RecentActivity } from './recentActivity.model';
#Injectable()
export class RecentActivityService {
private baseUrl = '/RecentActivity/';
constructor(private http: Http) {
}
get(): Observable<any> {
//return undefined;
return this.http
.get(this.baseUrl + 'Get')
.map((response: Response) => response.json())
//.toPromise()
;
}
}
recentActivity.spec.ts
import { async, describe, it, expect, inject, beforeEach, beforeEachProviders } from '#angular/core/testing';
import { Http, BaseRequestOptions, Response, ResponseOptions } from '#angular/http';
import { MockBackend, MockConnection } from '#angular/http/testing';
import { RecentActivity } from './recentActivity.model';
import { RecentActivityService } from './recentActivity.service';
describe('Recent Activity Service', () => {
let service: RecentActivityService;
let mockBackend: MockBackend;
const mockResponseData: RecentActivity[] = [
{ Name: 'Test Result 1', Url: '#/TestResult1' },
{ Name: 'Test Result 2', Url: '#/TestResult2' },
{ Name: 'Test Result 3', Url: '#/TestResult3' }
];
beforeEachProviders(() => [
RecentActivityService,
MockBackend,
BaseRequestOptions,
{
provide: Http,
useFactory: (backend, options) => new Http(backend, options),
deps: [MockBackend, BaseRequestOptions]
}
]);
beforeEach(inject([RecentActivityService, MockBackend], (ras, mb) => {
service = ras;
mockBackend = mb;
}));
it('Can load list of recent activities', (done) => {
mockBackend.connections.subscribe((connection: MockConnection) => {
const responseOpts = new ResponseOptions({ body: JSON.stringify(mockResponseData) });
connection.mockRespond(new Response(responseOpts));
});
service.get()
.subscribe((results: RecentActivity[]) => {
expect(results.length).toEqual(3);
expect(results[0].Name).toEqual('Test Result 1');
expect(results[1].Name).toEqual('Test Result 2');
expect(results[2].Name).toEqual('Test Result 3');
done();
});
});
});
unit-tests.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>Unit Tests</title>
<link rel="stylesheet" href="./libs/jasmine-core/lib/jasmine-core/jasmine.css">
<script src="./libs/jasmine-core/lib/jasmine-core/jasmine.js"></script>
<script src="./libs/jasmine-core/lib/jasmine-core/jasmine-html.js"></script>
<script src="./libs/jasmine-core/lib/jasmine-core/boot.js"></script>
<script src="./libs/core-js/client/shim.min.js"></script>
<script src="./libs/zone.js/dist/zone.js"></script>
<script src="./libs/reflect-metadata/Reflect.js"></script>
<script src="./libs/systemjs/dist/system.src.js"></script>
<script src="./libs/rxjs/bundles/Rx.js"></script>
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/underscore/underscore.js"></script>
<script src="~/lib/moment/moment.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="./systemjs.config.js"></script>
<script>
// #2. Configure systemjs to use the .js extension
// for imports from the app folder
System.config({
packages: {
'app': { defaultExtension: 'js' }
}
});
// #3. Import the spec file explicitly
Promise.all([
System.import('app/recentActivity/recentActivity.spec'),
System.import('app/pipes/styleVisibility.spec')
])
// #4. wait for all imports to load ...
// then re-execute `window.onload` which
// triggers the Jasmine test-runner start
// or explain what went wrong.
.then(window.onload)
.catch(console.error.bind(console));
</script>
</head>
<body>
</body>
</html>
I've tried to piece together the pieces to get this to work, but I can't figure out what I'm missing. Also as a side note I'm using Visual Studio 2015 and that is also giving a warning saying "Property map does not exist on type 'Observable'".
Like I mentioned everything works when I run this service for real and it returns my information from my backend no problem.

All I had to do was change the imports in the recentActivity.service.ts file to be
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
I still get an error (red squiggly) In Visual Studio, so if somebody could tell me how to get rid of that I would appreciate it.

The above solution works for me too. But since I was also using .do and .catch, I had to import those as well:
import 'rxjs/add/operator.do;
import 'rxjs/add/operator.catch;
Hope this helps others also.

Related

uni-app vue3 script setup could not find the globalData in other vue components

I use vue3 setup in uni-app and I am adding globalData but it does not work in setup sugar:
App.vue
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "#dcloudio/uni-app";
onLaunch(() => {
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
const globalData = {
text: 'mytext'
}
</script>
I could not get the globalData in my index.vue
const app = getApp() as any;
let text = app.globalData.text;
However,when I put it out of the setup then it works well.
App.vue
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "#dcloudio/uni-app";
onLaunch(() => {
console.log("App Launch");
});
onShow(() => {
console.log("App Show");
});
onHide(() => {
console.log("App Hide");
});
</script>
<script lang="ts">
export default {
globalData: {
text: 'mytext'
}
}
</script>
How to resolve it using setup?
From Vue 3 docs:
Components using <script setup> are closed by default - i.e. the public instance of the component, which is retrieved via template refs or $parent chains, will not expose any of the bindings declared inside <script setup>.
To explicitly expose properties in a <script setup> component, use the defineExpose compiler macro:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>
When a parent gets an instance of this component via template refs, the retrieved instance will be of the shape { a: number, b: number } (refs are automatically unwrapped just like on normal instances).
In your case, to expose globalData:
<script setup lang="ts">
⋮
const globalData = {
text: 'mytext'
}
defineExpose({ globalData })
</script>
demo

Vue3 custom element into Vue2 app using external framework

I have an application written in Vue2 which is not really ready to be upgraded to Vue3. But, I would like to start writing a component library in Vue3 and import the components back in Vue2 to eventually make the upgrade once it's ready.
Vue 3.2+ introduced defineCustomElement which works nicely but once I use a framework in the Vue3 environment (for example Quasar) that attaches to the Vue instance, it starts throwing errors in the Vue2 app, possibly because the result of defineCustomElement(SomeComponent) tries to use something from the framework that should be attached to the app.
I've thought about extending the HTMLElement and mounting the app on connectedCallback but then I lose the reactivity and have to manually handle all props/emits/.. like so:
class TestQuasarComponentCE extends HTMLElement {
// get init props
const prop1 = this.getAttribute('prop1')
// handle changes
// Mutation observer here probably...
const app = createApp(TestQuasarComponent, { prop1 }).use(Quasar)
app.mount(this)
}
customElements.define('test-quasar-component-ce', TestQuasarComponentCE);
So finally the question is - is it possible to somehow combine the defineCustomElement with a framework that attaches to the app?
So, after a bit of digging, I came up with the following.
First, let's create a component that uses our external library (Quasar in my case)
// SomeComponent.vue (Vue3 project)
<template>
<div class="container">
// This is the quasar component, it should get included in the build automatically if you use Vite/Vue-cli
<q-input
:model-value="message"
filled
rounded
#update:model-value="$emit('update:message', $event)"
/>
</div>
</template>
<script setup lang="ts>
defineProps({
message: { type: String }
})
defineEmits<{
(e: 'update:message', payload: string | number | null): void
}>()
</script>
Then we prepare the component to be built (this is where the magic happens)
// build.ts
import SomeComponent from 'path/to/SomeComponent.vue'
import { reactive } from 'vue'
import { Quasar } from 'quasar' // or any other external lib
const createCustomEvent = (name: string, args: any = []) => {
return new CustomEvent(name, {
bubbles: false,
composed: true,
cancelable: false,
detail: !args.length
? self
: args.length === 1
? args[0]
: args
});
};
class VueCustomComponent extends HTMLElement {
private _def: any;
private _props = reactive<Record<string, any>>({});
private _numberProps: string[];
constructor() {
super()
this._numberProps = [];
this._def = SomeComponent;
}
// Helper function to set the props based on the element's attributes (for primitive values) or properties (for arrays & objects)
private setAttr(attrName: string) {
// #ts-ignore
let val: string | number | null = this[attrName] || this.getAttribute(attrName);
if (val !== undefined && this._numberProps.includes(attrName)) {
val = Number(val);
}
this._props[attrName] = val;
}
// Mutation observer to handle attribute changes, basically two-way binding
private connectObserver() {
return new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.type === "attributes") {
const attrName = mutation.attributeName as string;
this.setAttr(attrName);
}
});
});
}
// Make emits available at the parent element
private createEventProxies() {
const eventNames = this._def.emits as string[];
if (eventNames) {
eventNames.forEach(evName => {
const handlerName = `on${evName[0].toUpperCase()}${evName.substring(1)}`;
this._props[handlerName] = (...args: any[]) => {
this.dispatchEvent(createCustomEvent(evName, args));
};
});
}
}
// Create the application instance and render the component
private createApp() {
const self = this;
const app = createApp({
render() {
return h(self._def, self._props);
}
})
.use(Quasar);
// USE ANYTHING YOU NEED HERE
app.mount(this);
}
// Handle element being inserted into DOM
connectedCallback() {
const componentProps = Object.entries(SomeComponent.props);
componentProps.forEach(([propName, propDetail]) => {
// #ts-ignore
if (propDetail.type === Number) {
this._numberProps.push(propName);
}
this.setAttr(propName);
});
this.createEventProxies();
this.createApp();
this.connectObserver().observe(this, { attributes: true });
}
}
// Register as custom element
customElements.define('some-component-ce', VueCustomElement);
Now, we need to build it as library (I use Vite, but should work for vue-cli as well)
// vite.config.ts
export default defineConfig({
...your config here...,
build: {
lib: {
entry: 'path/to/build.ts',
name: 'ComponentsLib',
fileName: format => `components-lib.${format}.js`
}
}
})
Now we need to import the built library in a context that has Vue3, in my case index.html works fine.
// index.html (Vue2 project)
<!DOCTYPE html>
<html lang="">
<head>
// Vue3
<script src="https://cdn.jsdelivr.net/npm/vue#3/dist/vue.global.prod.js"></script>
// Quasar styles
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons" rel="stylesheet" type="text/css">
<link href="https://cdn.jsdelivr.net/npm/quasar#2.4.3/dist/quasar.prod.css" rel="stylesheet" type="text/css">
// Our built component
<script src="path/to/components-lib.umd.js"></script>
</head>
...rest of your html...
</html>
Now we are ready to use our component within our Vue2 (or any other) codebase same way we are used to with some minor changes, check comments below.
// App.vue (Vue2 project)
<template>
<some-component-ce
:message="message" // For primitive values
:obj.prop="obj" // Notice the .prop there -> for arrays & objects
#update:message="message = $event.detail" // Notice the .detail here
/>
</template>
<script>
export default {
data() {
return {
message: 'Some message here',
obj: { x: 1, y: 2 },
}
}
}
</script>
Now, you can use Vue3 components in Vue2 :)

VueJS PSPDFKit Integration

I am trying to integrate PSPDFKit (standalone) with a VueJS project, but struggling with some unexpected behaviour.
For background:
I have a component which includes the import PSPDFKit from 'pspdfkit' statement. When I run the Vue application and navigate to the view that utilises this component, I get an Uncaught SyntaxError: Unexpected token '<' - suggesting that I'm encountering HTML instead of the intended library.
However, when running locally, if I comment out the import line, wait for the PSPDFKit is not defined errors, then un-comment the import line, the viewer loads as expected.
Can anyone shed any light on what's happening here? I'm struggling to diagnose the issue and ensure the integration works as expected on first load. Component code is below.
Thanks
<template>
<div class="container"></div>
</template>
<script>
import PSPDFKit from 'pspdfkit'
export default {
name: 'PSPDFKitWrapper',
props: ["documentUrl", "licenseKey", "baseUrl"],
_instance: null,
mounted: function ()
{
this.load();
},
methods: {
load()
{
PSPDFKit.load({
document: this.documentUrl,
container: ".container",
licenseKey: this.licenseKey,
baseUrl: this.baseUrl
})
.then(instance =>
{
this._instance = instance;
this.$emit("update:error", "");
})
.catch(err =>
{
PSPDFKit.unload(".container");
this.$emit("update:error", err.message);
});
},
unload()
{
if (this._instance) {
PSPDFKit.unload(this._instance);
this._instance = null;
} else {
PSPDFKit.unload(".container");
}
}
}
}
</script>
<style scoped>
.container {
width: 100%;
height: 90vh;
}
</style>

TypeScript compiler can't find injected property in Vue Object Component

I'm writing a couple of examples for work, and one that's hanging me up is injecting a service that's provided during Vue's bootstrapping.
This "works" (I can access it, it compiles, and runs), and there are no problems or complaints with my JavaScript version nor the Class-Component TypeScript version, but the compiler complains that this.sampleService doesn't exist in my component when using the Vue object API with TypeScript.
Am I missing something?
<template>
<div class="app">
{{message}} <fake-button :text="buttonText" #some-click-event="handleClickEvent"></fake-button>
</div>
</template>
<style lang="scss">
.app {
$background-color: #9f9;
$foreground-color: #000;
background: $background-color;
color: $foreground-color;
}
</style>
<script lang="ts">
import Vue from 'vue'
const App = Vue.extend({
components: {
FakeButton: () => import('#client/components/fake-button/fake-button-object-typescript.vue')
},
data: function () {
return {
message: 'Hello World - App Object TypeScript',
buttonText: 'Click Me'
}
},
inject: {
sampleService: 'sampleService'
},
methods: {
handleClickEvent(someVal?: string) {
console.log('App', 'handleClickEvent', someVal);
}
},
beforeCreate() {
console.log('App', 'beforeCreate');
},
created() {
console.log('App', 'created');
},
mounted() {
console.log('App', 'mounted');
// TODO: While this compiles, TypeScript complains that it doesn't exist
console.log('this.sampleService.getDate()', this.sampleService.getDate());
}
});
export default App;
</script>
ERROR in vue-test/src/client/containers/app/app-object-typescript.vue.ts
[tsl] ERROR in vue-test/src/client/containers/app/app-object-typescript.vue.ts(35,56)
TS2339: Property 'sampleService' does not exist on type 'CombinedVueInstance<Vue, { message: string; buttonText: string; }, { handleClickEvent(someVal?: s...'.
My solution for this problem was to create a interface for the injection. In you example that would be something like the follwing:
<script lang="ts">
import { SampleServiceInjection } from '...';
const App = ( Vue as VueConstructor<Vue & SampleServiceInjection> ).extend({
inject: {
sampleService: 'sampleService'
} as Record<keyof SampleServiceInjection, string>,
// etc.
});
</script>
You can use that very interface in component that provides the service as well:
export interface SampleServiceInjection {
sampleService: SampleService; // or whatever type your service has
}
export default Vue.extend({
provide(): SampleServiceInjection {
return {
sampleService: new SampleService(),
};
},
// etc.
});
Try adding provide. Check example below. I would suggest using vue-property-decorator since your are leveraging typescript.
inject: {
sampleService: 'sampleService'
},
provide () {
return {
sampleService: this.sampleService,
}
}

Angular 2 tutorial - part 4: "SyntaxError: Unexpected token <(…)"

EDIT: I tried to apply the fix #Thierry provided in his answer, however I keep getting the same error.
I created a repository with the full project (clean with no comments) as it resulted by following the tutorial and after applying #Thierry's fix: https://github.com/dragGH102/angular2-tutorial-part4-test
I am following tutorial for Angular 2 at https://angular.io/docs/ts/latest/tutorial/toh-pt4.html
At the end of part 4 I am getting the following error:
SyntaxError: Unexpected token <(…)Zone.run # angular2-polyfills.js:1243
I even tried to:
copy-paste the Plunkr provided at http://plnkr.co/edit/?p=preview but same error.
remove the Promise part (which seems to be the cause of the error based on the stacktrace below)
compile TS files myself (instead of letting it do by "npm start")
Error stacktrace
SyntaxError: Unexpected token <(…)
Zone.run # angular2-polyfills.js:1243
zoneBoundFn # angular2-polyfills.js:1220
lib$es6$promise$$internal$$tryCatch # angular2-polyfills.js:468
lib$es6$promise$$internal$$invokeCallback # angular2-polyfills.js:480
lib$es6$promise$$internal$$publish # angular2-polyfills.js:451
lib$es6$promise$$internal$$publishRejection # angular2-polyfills.js:401
(anonymous function) # angular2-polyfills.js:123
Zone.run # angular2-polyfills.js:1243
zoneBoundFn # angular2-polyfills.js:1220
lib$es6$promise$asap$$flush # angular2-polyfills.js:262
Apparently (e.g. angular2: Uncaught SyntaxError: Unexpected token < ) this is due to the browser not find valid JS code due to an error but I can't figure out what's wrong .
Here is my code (also available at http://plnkr.co/edit/Q5F0mV8Hbdcr2rtZUSw5 )
index.html
<html>
<head>
<title>Angular 2 QuickStart</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<script src="node_modules/es6-shim/es6-shim.min.js"></script>
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="node_modules/rxjs/bundles/Rx.js"></script>
<script src="node_modules/angular2/bundles/angular2.dev.js"></script>
<script>
System.config({
transpiler: 'typescript',
typescriptOptions: { emitDecoratorMetadata: true },
packages: {'app': {defaultExtension: 'ts'}}
});
System.import('app/boot')
.then(null, console.error.bind(console));
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
</html>
app.component.ts
import {Component, OnInit} from 'angular2/core';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';
#Component({
selector: 'my-app',
template:`
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="#hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<my-hero-detail [hero]="selectedHero"></my-hero-detail>
`,
// for the sake of code readibility I removed "styles"
directives: [HeroDetailComponent],
providers: [HeroService],
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private _heroService: HeroService) { }
getHeroes() {
this.heroes = this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero) { this.selectedHero = hero; }
}
boot.ts
import {bootstrap} from 'angular2/platform/browser'
import {AppComponent} from './app.component'
bootstrap(AppComponent);
hero-detail.component.ts
import {Component} from 'angular2/core';
import {Hero} from './hero';
#Component({
selector: 'my-hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div><label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>`,
inputs: ['hero']
})
export class HeroDetailComponent {
hero: Hero;
}
hero.service.ts
import {Hero} from './hero';
import {HEROES} from './mock-heroes';
import {Injectable} from 'angular2/core';
#Injectable()
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
}
hero.ts
export interface Hero {
id: number;
name: string;
}
mock-heroes.ts
import {Hero} from "./hero";
export var HEROES: Hero[] = [
{ "id": 11, "name": "Mr. Nice" },
{ "id": 12, "name": "Narco" },
{ "id": 13, "name": "Bombasto" },
{ "id": 14, "name": "Celeritas" },
{ "id": 15, "name": "Magneta" },
{ "id": 16, "name": "RubberMan" },
{ "id": 17, "name": "Dynama" },
{ "id": 18, "name": "Dr IQ" },
{ "id": 19, "name": "Magma" },
{ "id": 20, "name": "Tornado" }
];
I had a look at your plunkr and the problem comes from this line:
getHeroes() {
this.heroes = this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
In fact, you mix two approaches:
either you set the promise on a component property and then the async pipe to display data when the promise is resolved
#Component({
selector: 'my-app',
template:`
<li *ngFor="#hero of heroes">
(...)
</li>
`
})
export class AppComponent implements OnInit {
getHeroes() {
this.heroes = this._heroService.getHeroes();
}
}
either you call the then method and set the received data when resolving into a component property. In this case, you don't need the async pipe
#Component({
selector: 'my-app',
template:`
<li *ngFor="#hero of heroes">
(...)
</li>
`
})
export class AppComponent implements OnInit {
getHeroes() {
this._heroService.getHeroes().then(heroes => this.heroes = heroes);
}
}
See my updates in a forked plunkr: http://plnkr.co/edit/Rw4kOzKFbVosaoe7dRL1?p=preview.
Otherwise I can't see any error like SyntaxError: Unexpected token <(…) but most of time it's because you try to load a JS file and you get a 404 error...
See this question for more details:
How to debug Angular2 in Chrome