feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+5
View File
@@ -0,0 +1,5 @@
language: node_js
node_js:
- 10
cache: yarn
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Ian Webster
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+172
View File
@@ -0,0 +1,172 @@
Chart.js to Image
---
[![npm](https://img.shields.io/npm/v/chartjs-to-image)](https://www.npmjs.com/package/chartjs-to-image)
[![npm](https://img.shields.io/npm/dt/chartjs-to-image)](https://www.npmjs.com/package/chartjs-to-image)
[![Build Status](https://app.travis-ci.com/typpo/chartjs-to-image.svg?branch=main)](https://app.travis-ci.com/typpo/chartjs-to-image)
This is a wrapper for exporting Chart.js as an image. It works on the server side as well as client side (although on the client you may prefer to use [toBase64Image](https://quickchart.io/documentation/chart-js/image-export/#use-tobase64image-in-the-browser)).
The renderer is based on QuickChart, a free and open-source web service for generating static charts. View the main QuickChart repository [here](https://github.com/typpo/quickchart).
# Installation
If you are using npm:
```
npm install chartjs-to-image
```
# Usage
This library provides a **ChartJsImage** object. Import it, instantiate it, and set your [Chart.js](https://www.chartjs.org) config:
```js
const ChartJsImage = require('chartjs-to-image');
const myChart = new ChartJsImage();
myChart.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
```
Write the image to disk:
```js
myChart.toFile('/tmp/mychart.png');
```
Convert it to a data URL:
```js
const dataUrl = await myChart.toDataUrl();
```
Or get a buffer that contains your chart image:
```js
const buf = await myChart.toBinary();
```
ChartJsImage supports some additional functions using a third party rendering service. Use `getUrl()` on your ChartJsImage object to obtain a URL that will display your chart when visited:
```js
console.log(myChart.getUrl());
// Prints: https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png
```
For larger charts, you may not want to encode the chart in the URL. Use `getShortUrl()` to get a fixed-length URL:
```js
const url = await myChart.getShortUrl();
console.log(url);
// Prints: https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401
```
All the above examples create this following Chart.js image:
<img src="https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png" width=500 />
## Customizing your chart
### setConfig(chart)
Use this config to customize the Chart.js config object that defines your chart. You must set this before creating any outputs!
### setChartJsVersion(version: string)
Sets the version of Chart.js to use. Defaults to the latest version of Chart.js v2. Other valid version settings include: "3", "4", "3.9.1", "4.1.1", etc.
### setWidth(width: int)
Sets the width of the chart in pixels. Defaults to 500.
### setHeight(height: int)
Sets the height of the chart in pixels. Defaults to 300.
### setFormat(format: string)
Sets the format of the chart. Defaults to `png`. `svg` and `webp` are also valid.
### setBackgroundColor(color: string)
Sets the background color of the chart. Any valid HTML color works. Defaults to `#ffffff` (white). Also takes `rgb`, `rgba`, and `hsl` values.
### setDevicePixelRatio(ratio: float)
Sets the device pixel ratio of the chart. This will multiply the number of pixels by the value. This is usually used for retina displays. Defaults to 1.0.
## Getting outputs
There are two ways to get a URL for your chart object.
### getUrl(): string
Returns a URL that will display the chart image when loaded.
### getShortUrl(): Promise<string>
Uses the quickchart.io web service to create a fixed-length chart URL that displays the chart image. The Promise resolves with a URL such as `https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401`.
Note that short URLs expire after a few days for users of the free service. You can [subscribe](https://quickchart.io/pricing/) to keep them around longer.
### toBinary(): Promise<Buffer>
Creates a binary buffer that contains your chart image.
### toDataUrl(): Promise<string>
Returns a base 64 data URL beginning with `data:image/png;base64`.
### toFile(pathOrDescriptor: string): Promise
Creates a file containing your chart image.
## More examples
Check out the `examples/` directory to see other usage. Here's a simple test that uses some of the custom parameters:
```js
const chart = new ChartJsImage();
chart.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
chart.setWidth(500).setHeight(300).setBackgroundColor('transparent');
console.log(chart.getUrl());
// https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png
chart.toFile('/tmp/test.png')
```
Here's a more complicated chart that includes some Javascript:
```js
chart.setConfig({
type: 'bar',
data: {
labels: ['January', 'February', 'March', 'April', 'May'],
datasets: [
{
label: 'Dogs',
data: [50, 60, 70, 180, 190],
},
],
},
options: {
scales: {
yAxes: [
{
ticks: {
callback: function (value) {
return '$' + value;
},
},
},
],
},
},
});
chart.setWidth(500).setHeight(300).setBackgroundColor('#0febc2');
const buf = await chart.toBinary();
```
@@ -0,0 +1,33 @@
const ChartJsImage = require('../index');
const chart = new ChartJsImage();
chart.setConfig({
type: 'bar',
data: {
labels: ['January', 'February', 'March', 'April', 'May'],
datasets: [
{
label: 'Dogs',
data: [50, 60, 70, 180, 190],
},
],
},
options: {
scales: {
yAxes: [
{
ticks: {
callback: function (value) {
return '$' + value;
},
},
},
],
},
},
});
chart.setWidth(500).setHeight(300).setBackgroundColor('#0febc2');
console.log(chart.getUrl());
// https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27January%27%2C%27February%27%2C%27March%27%2C%27April%27%2C%27May%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Dogs%27%2Cdata%3A%5B50%2C60%2C70%2C180%2C190%5D%7D%5D%7D%2Coptions%3A%7Bscales%3A%7ByAxes%3A%5B%7Bticks%3A%7Bcallback%3Afunction+%28value%29+%7B%0A++return+%27%24%27+%2B+value%3B%0A%7D%7D%7D%5D%7D%7D%7D&w=500&h=300&bkg=%230febc2&f=png
@@ -0,0 +1,23 @@
const ChartJsImage = require('../index');
const chart = new ChartJsImage();
// Fill the chart with data from 0 to 100
const data = [...Array(100).keys()];
chart.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data }] },
});
// Print the regular URL...
console.log(chart.getUrl());
// https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B0%2C1%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%2C33%2C34%2C35%2C36%2C37%2C38%2C39%2C40%2C41%2C42%2C43%2C44%2C45%2C46%2C47%2C48%2C49%2C50%2C51%2C52%2C53%2C54%2C55%2C56%2C57%2C58%2C59%2C60%2C61%2C62%2C63%2C64%2C65%2C66%2C67%2C68%2C69%2C70%2C71%2C72%2C73%2C74%2C75%2C76%2C77%2C78%2C79%2C80%2C81%2C82%2C83%2C84%2C85%2C86%2C87%2C88%2C89%2C90%2C91%2C92%2C93%2C94%2C95%2C96%2C97%2C98%2C99%5D%7D%5D%7D%7D&w=500&h=300&bkg=%23ffffff&f=png
// That's a long URL! Maybe we want a shorter version (requires an HTTP request to QuickChart.io)
async function printShortUrl() {
const url = await chart.getShortUrl();
console.log(url);
}
printShortUrl();
// https://quickchart.io/chart/render/f-a1d3e804-dfea-442c-88b0-9801b9808401
// Much shorter and more manageable :)
@@ -0,0 +1,12 @@
const ChartJsImage = require('../index');
const chart = new ChartJsImage();
chart.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
chart.setWidth(500).setHeight(300).setBackgroundColor('transparent');
console.log(chart.getUrl());
// https://quickchart.io/chart?c=%7Btype%3A%27bar%27%2Cdata%3A%7Blabels%3A%5B%27Hello+world%27%2C%27Foo+bar%27%5D%2Cdatasets%3A%5B%7Blabel%3A%27Foo%27%2Cdata%3A%5B1%2C2%5D%7D%5D%7D%7D&w=500&h=300&bkg=transparent&f=png
@@ -0,0 +1,17 @@
const fs = require('fs');
const ChartJsImage = require('../index');
const chart = new ChartJsImage();
chart.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
chart.setWidth(500).setHeight(300).setBackgroundColor('transparent');
async function saveChart() {
// Write file to disk
await chart.toFile('/tmp/chart.png');
}
saveChart();
+28
View File
@@ -0,0 +1,28 @@
declare class ChartJsImage {
constructor(apiKey?: string, accountId?: string);
setConfig(chartConfig: object): this;
setWidth(width: number | string): this;
setHeight(height: number | string): this;
setBackgroundColor(color: string): this;
setDevicePixelRatio(ratio: number | string): this;
setFormat(fmt: string): this;
setChartJsVersion(version: string): this;
isValid(): boolean;
getUrl(): string;
getPostData(): {
width: number;
height: number;
chart: string;
format?: string;
backgroundColor?: string;
devicePixelRatio?: number;
version?: string;
};
getShortUrl(): Promise<string>;
toBinary(): Promise<Buffer>;
toDataUrl(): Promise<string>;
toFile(pathOrDescriptor: string): Promise<void>;
}
export = ChartJsImage;
+152
View File
@@ -0,0 +1,152 @@
const fs = require('fs');
const axios = require('axios');
const { stringify } = require('javascript-stringify');
class ChartJsImage {
constructor(apiKey, accountId) {
this.apiKey = apiKey;
this.accountId = accountId;
this.host = 'quickchart.io';
this.protocol = 'https';
this.baseUrl = `${this.protocol}://${this.host}`;
this.chart = undefined;
this.width = 500;
this.height = 300;
this.devicePixelRatio = 1.0;
this.backgroundColor = '#ffffff';
this.format = 'png';
this.version = '2';
}
setConfig(chartConfig) {
this.chart = stringify(chartConfig);
return this;
}
setWidth(width) {
this.width = parseInt(width, 10);
return this;
}
setHeight(height) {
this.height = parseInt(height, 10);
return this;
}
setBackgroundColor(color) {
this.backgroundColor = color;
return this;
}
setDevicePixelRatio(ratio) {
this.devicePixelRatio = parseFloat(ratio);
return this;
}
setFormat(fmt) {
this.format = fmt;
return this;
}
setChartJsVersion(version) {
this.version = version;
return this;
}
isValid() {
if (!this.chart) {
return false;
}
return true;
}
getUrl() {
if (!this.isValid()) {
throw new Error('You must call setConfig before getUrl');
}
const ret = new URL(`${this.baseUrl}/chart`);
ret.searchParams.append('c', this.chart);
ret.searchParams.append('w', this.width);
ret.searchParams.append('h', this.height);
if (this.devicePixelRatio !== 1.0) {
ret.searchParams.append('devicePixelRatio', this.devicePixelRatio);
}
if (this.backgroundColor !== 1.0) {
ret.searchParams.append('bkg', this.backgroundColor);
}
if (this.format !== 1.0) {
ret.searchParams.append('f', this.format);
}
if (this.version) {
ret.searchParams.append('v', this.version);
}
return ret.href;
}
getPostData() {
const { width, height, chart, format, backgroundColor, devicePixelRatio, version } = this;
const postData = {
width,
height,
chart,
};
if (format) {
postData.format = format;
}
if (backgroundColor) {
postData.backgroundColor = backgroundColor;
}
if (devicePixelRatio) {
postData.devicePixelRatio = devicePixelRatio;
}
if (version) {
postData.version = version;
}
return postData;
}
async getShortUrl() {
if (!this.isValid()) {
throw new Error('You must call setConfig before getUrl');
}
const resp = await axios.post('https://quickchart.io/chart/create', this.getPostData());
if (resp.status !== 200) {
throw `Bad response code ${resp.status} from chart shorturl endpoint`;
} else if (!resp.data.success) {
throw 'Received failure response from chart shorturl endpoint';
} else {
return resp.data.url;
}
}
async toBinary() {
if (!this.isValid()) {
throw new Error('You must call setConfig before getUrl');
}
const resp = await axios.post('https://quickchart.io/chart', this.getPostData(), {
responseType: 'arraybuffer',
});
if (resp.status !== 200) {
throw `Bad response code ${resp.status} from chart shorturl endpoint`;
}
return Buffer.from(resp.data, 'binary');
}
async toDataUrl() {
const buf = await this.toBinary();
const b64buf = buf.toString('base64');
return `data:image/png;base64,${b64buf}`;
}
async toFile(pathOrDescriptor) {
const buf = await this.toBinary();
fs.writeFileSync(pathOrDescriptor, buf);
}
}
module.exports = ChartJsImage;
+26
View File
@@ -0,0 +1,26 @@
{
"name": "chartjs-to-image",
"version": "1.2.2",
"description": "Convert Chart.js to image",
"main": "index.js",
"repository": "https://github.com/typpo/chartjs-to-image",
"author": "Ian Webster",
"license": "MIT",
"scripts": {
"test": "jest",
"format": "prettier --single-quote --trailing-comma all --print-width 100 --write \"**/*.js\""
},
"dependencies": {
"axios": "^1.6.0",
"javascript-stringify": "^2.1.0"
},
"devDependencies": {
"jest": "^27.4.3",
"prettier": "^2.0.5"
},
"jest": {
"moduleNameMapper": {
"axios": "axios/dist/node/axios.cjs"
}
}
}
+269
View File
@@ -0,0 +1,269 @@
const axios = require('axios');
const ChartJsImage = require('../index');
jest.mock('axios');
test('basic chart, no auth', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
expect(qc.getUrl()).toContain('Hello+world');
expect(qc.getUrl()).toContain('/chart?');
expect(qc.getUrl()).toContain('w=500');
expect(qc.getUrl()).toContain('h=300');
expect(qc.getUrl()).toContain('v=2');
});
test('basic chart, width and height', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
qc.setWidth(800).setHeight(500);
expect(qc.getUrl()).toContain('Hello+world');
expect(qc.getUrl()).toContain('w=800');
expect(qc.getUrl()).toContain('h=500');
});
test('basic chart, other params', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
qc.setBackgroundColor('#000000').setDevicePixelRatio(2.0).setFormat('svg');
expect(qc.getUrl()).toContain('Hello+world');
expect(qc.getUrl()).toContain('devicePixelRatio=2');
expect(qc.getUrl()).toContain('f=svg');
expect(qc.getUrl()).toContain('bkg=%23000000');
});
test('basic chart, version', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
qc.setChartJsVersion('3.9.1');
expect(qc.getUrl()).toContain('Hello+world');
expect(qc.getUrl()).toContain('&v=3.9.1');
});
test('js chart', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: {
labels: ['January', 'February', 'March', 'April', 'May'],
datasets: [
{
label: 'Dogs',
data: [50, 60, 70, 180, 190],
},
],
},
options: {
scales: {
yAxes: [
{
ticks: {
callback: function (value) {
return '$' + value;
},
},
},
],
},
},
});
expect(qc.getUrl()).toContain('Dogs');
expect(qc.getUrl()).toContain('callback%3Afunction+%28value');
});
test('postdata for basic chart, no auth', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
const postData = qc.getPostData();
expect(postData.chart).toContain('Hello world');
expect(postData.width).toEqual(500);
expect(postData.height).toEqual(300);
expect(postData.format).toEqual('png');
expect(postData.backgroundColor).toEqual('#ffffff');
expect(postData.devicePixelRatio).toBeCloseTo(1);
});
test('postdata for basic chart with params', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
qc.setWidth(400)
.setHeight(200)
.setFormat('svg')
.setBackgroundColor('transparent')
.setDevicePixelRatio(2.0);
const postData = qc.getPostData();
expect(postData.chart).toContain('Hello world');
expect(postData.width).toEqual(400);
expect(postData.height).toEqual(200);
expect(postData.format).toEqual('svg');
expect(postData.backgroundColor).toEqual('transparent');
expect(postData.devicePixelRatio).toBeCloseTo(2);
});
test('postdata for js chart', () => {
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: {
labels: ['January', 'February', 'March', 'April', 'May'],
datasets: [
{
label: 'Dogs',
data: [50, 60, 70, 180, 190],
},
],
},
options: {
scales: {
yAxes: [
{
ticks: {
callback: function (value) {
return '$' + value;
},
},
},
],
},
},
});
qc.setWidth(400)
.setHeight(200)
.setFormat('svg')
.setBackgroundColor('transparent')
.setDevicePixelRatio(2.0);
const postData = qc.getPostData();
expect(postData.chart).toContain('callback:function (val');
expect(postData.width).toEqual(400);
expect(postData.height).toEqual(200);
expect(postData.format).toEqual('svg');
expect(postData.backgroundColor).toEqual('transparent');
expect(postData.devicePixelRatio).toBeCloseTo(2);
});
test('getShortUrl for chart, no auth', async () => {
const mockResp = {
status: 200,
data: {
success: true,
url: 'https://ChartJsImage.io/chart/render/9a560ba4-ab71-4d1e-89ea-ce4741e9d232',
},
};
axios.post.mockImplementationOnce(() => Promise.resolve(mockResp));
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
await expect(qc.getShortUrl()).resolves.toEqual(mockResp.data.url);
expect(axios.post).toHaveBeenCalled();
});
test('getShortUrl for chart bad status code', async () => {
const mockResp = {
status: 502,
};
axios.post.mockImplementationOnce(() => Promise.resolve(mockResp));
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
await expect(qc.getShortUrl()).rejects.toContain('Bad response code');
expect(axios.post).toHaveBeenCalled();
});
test('getShortUrl api failure', async () => {
const mockResp = {
status: 200,
data: {
success: false,
},
};
axios.post.mockImplementationOnce(() => Promise.resolve(mockResp));
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
await expect(qc.getShortUrl()).rejects.toContain('failure response');
expect(axios.post).toHaveBeenCalled();
});
test('toBinary, no auth', async () => {
const mockResp = {
status: 200,
data: Buffer.from('bWVvdw==', 'base64'),
};
axios.post.mockImplementationOnce(() => Promise.resolve(mockResp));
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
await expect(qc.toBinary()).resolves.toEqual(mockResp.data);
expect(axios.post).toHaveBeenCalled();
});
test('toBinary, no auth', async () => {
const mockResp = {
status: 200,
data: Buffer.from('bWVvdw==', 'base64'),
};
axios.post.mockImplementationOnce(() => Promise.resolve(mockResp));
const qc = new ChartJsImage();
qc.setConfig({
type: 'bar',
data: { labels: ['Hello world', 'Foo bar'], datasets: [{ label: 'Foo', data: [1, 2] }] },
});
await expect(qc.toDataUrl()).resolves.toEqual('data:image/png;base64,bWVvdw==');
expect(axios.post).toHaveBeenCalled();
});
test('no chart specified throws error', async () => {
const qc = new ChartJsImage();
expect(() => {
qc.getUrl();
}).toThrow();
});