Merge branch 'dev' of gitlab.com:ligolang/ligo into rinderknecht@contracts
This commit is contained in:
commit
604330eab6
@ -7,6 +7,7 @@ import { AppState } from '../redux/app';
|
||||
import { CommandState } from '../redux/command';
|
||||
import { DoneLoadingAction, LoadingState } from '../redux/loading';
|
||||
import { ResultState } from '../redux/result';
|
||||
import { Command } from '../redux/types';
|
||||
import { OutputToolbarComponent } from './output-toolbar';
|
||||
|
||||
const Container = styled.div<{ visible?: boolean }>`
|
||||
@ -43,7 +44,7 @@ const CancelButton = styled.div`
|
||||
|
||||
const Output = styled.div`
|
||||
flex: 1;
|
||||
padding: 0 0.5em 0.5em 0.5em;
|
||||
padding: 0.5em;
|
||||
display: flex;
|
||||
overflow: scroll;
|
||||
/* This font size is used to calcuate spinner size */
|
||||
@ -81,12 +82,11 @@ function copyOutput(el: HTMLElement | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function downloadOutput(el: HTMLElement | null) {
|
||||
if (el) {
|
||||
function downloadOutput(output: string) {
|
||||
const anchor = document.createElement('a');
|
||||
anchor.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' + encodeURIComponent(el.innerHTML)
|
||||
`data:text/plain;charset=utf-8,${encodeURIComponent(output)}`
|
||||
);
|
||||
anchor.setAttribute('download', 'output.txt');
|
||||
|
||||
@ -94,7 +94,6 @@ function downloadOutput(el: HTMLElement | null) {
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
export const OutputTabComponent = (props: {
|
||||
@ -107,6 +106,9 @@ export const OutputTabComponent = (props: {
|
||||
const contract = useSelector<AppState, ResultState['contract']>(
|
||||
state => state.result.contract
|
||||
);
|
||||
const command = useSelector<AppState, ResultState['command']>(
|
||||
state => state.result.command
|
||||
);
|
||||
|
||||
const loading = useSelector<AppState, LoadingState>(state => state.loading);
|
||||
|
||||
@ -132,10 +134,14 @@ export const OutputTabComponent = (props: {
|
||||
|
||||
return (
|
||||
<Container visible={props.selected}>
|
||||
{output.length !== 0 && (
|
||||
{!(
|
||||
loading.loading ||
|
||||
output.length === 0 ||
|
||||
command !== Command.Compile
|
||||
) && (
|
||||
<OutputToolbarComponent
|
||||
onCopy={() => copyOutput(preRef.current)}
|
||||
onDownload={() => downloadOutput(preRef.current)}
|
||||
onDownload={() => downloadOutput(output)}
|
||||
></OutputToolbarComponent>
|
||||
)}
|
||||
<Output id="output" ref={outputRef}>
|
||||
|
@ -1,78 +1,58 @@
|
||||
import { faCopy, faDownload } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AppState } from '../redux/app';
|
||||
import { ResultState } from '../redux/result';
|
||||
import { Item, Toolbar } from './toolbar';
|
||||
import { Tooltip } from './tooltip';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 0.2em 0.5em;
|
||||
z-index: 3;
|
||||
const Divider = styled.div`
|
||||
display: block;
|
||||
background-color: rgba(0, 0, 0, 0.12);
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
margin: 0 3px;
|
||||
`;
|
||||
|
||||
const Action = styled.div`
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
margin: 4px 6px;
|
||||
cursor: pointer;
|
||||
|
||||
opacity: 0.5;
|
||||
color: #444;
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:hover::before {
|
||||
background-color: rgba(32, 33, 36, 0.059);
|
||||
const Link = styled.a`
|
||||
font-size: 0.8em;
|
||||
color: var(--blue);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const OutputToolbarComponent = (props: {
|
||||
onCopy?: () => void;
|
||||
onDownload?: () => void;
|
||||
}) => {
|
||||
const output = useSelector<AppState, ResultState['output']>(
|
||||
state => state.result.output
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Action onClick={() => props.onCopy && props.onCopy()}>
|
||||
<Toolbar>
|
||||
<Item onClick={() => props.onCopy && props.onCopy()}>
|
||||
<FontAwesomeIcon icon={faCopy}></FontAwesomeIcon>
|
||||
<Tooltip>Copy</Tooltip>
|
||||
</Action>
|
||||
<Action onClick={() => props.onDownload && props.onDownload()}>
|
||||
</Item>
|
||||
<Item onClick={() => props.onDownload && props.onDownload()}>
|
||||
<FontAwesomeIcon icon={faDownload}></FontAwesomeIcon>
|
||||
<Tooltip>Download</Tooltip>
|
||||
</Action>
|
||||
</Container>
|
||||
</Item>
|
||||
<Divider></Divider>
|
||||
<Item>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://try-michelson.tzalpha.net/?source=${encodeURIComponent(
|
||||
output
|
||||
)}`}
|
||||
>
|
||||
View in Try-Michelson IDE
|
||||
</Link>
|
||||
</Item>
|
||||
</Toolbar>
|
||||
);
|
||||
};
|
||||
|
65
tools/webide/packages/client/src/components/toolbar.tsx
Normal file
65
tools/webide/packages/client/src/components/toolbar.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.2em 0.5em;
|
||||
z-index: 3;
|
||||
`;
|
||||
|
||||
export const Group = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Item = styled.div`
|
||||
z-index: 3;
|
||||
position: relative;
|
||||
margin: 4px 6px;
|
||||
cursor: pointer;
|
||||
|
||||
opacity: 0.5;
|
||||
color: #444;
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
bottom: -4px;
|
||||
left: -4px;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
border-radius: 4px;
|
||||
background: none;
|
||||
box-sizing: border-box;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition-property: transform, opacity;
|
||||
transition-duration: 0.15s;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
:hover::before {
|
||||
background-color: rgba(32, 33, 36, 0.059);
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Toolbar = (props: any) => {
|
||||
return <Container>{props.children}</Container>;
|
||||
};
|
@ -48,7 +48,7 @@
|
||||
--font_ghost_weight: 700;
|
||||
--font_ghost_color: rgba(153, 153, 153, 0.5); /* or #CFCFCF */
|
||||
|
||||
--content_height: 85vh;
|
||||
--content_height: 84vh;
|
||||
|
||||
--tooltip_foreground: white;
|
||||
--tooltip_background: rgba(0, 0, 0, 0.75) /*#404040*/;
|
||||
|
@ -4,6 +4,7 @@ import { compileContract, getErrorMessage } from '../../services/api';
|
||||
import { AppState } from '../app';
|
||||
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
|
||||
import { ChangeOutputAction } from '../result';
|
||||
import { Command } from '../types';
|
||||
import { CancellableAction } from './cancellable';
|
||||
|
||||
export class CompileAction extends CancellableAction {
|
||||
@ -24,13 +25,18 @@ export class CompileAction extends CancellableAction {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ ...new ChangeOutputAction(michelsonCode.result) });
|
||||
dispatch({
|
||||
...new ChangeOutputAction(michelsonCode.result, Command.Compile)
|
||||
});
|
||||
} catch (ex) {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
|
||||
...new ChangeOutputAction(
|
||||
`Error: ${getErrorMessage(ex)}`,
|
||||
Command.Compile
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2,11 +2,12 @@ import { Tezos } from '@taquito/taquito';
|
||||
import { TezBridgeSigner } from '@taquito/tezbridge-signer';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { compileContract, compileExpression, deploy, getErrorMessage } from '../../services/api';
|
||||
import { compileContract, compileStorage, deploy, getErrorMessage } from '../../services/api';
|
||||
import { AppState } from '../app';
|
||||
import { MichelsonFormat } from '../compile';
|
||||
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
|
||||
import { ChangeContractAction, ChangeOutputAction } from '../result';
|
||||
import { Command } from '../types';
|
||||
import { CancellableAction } from './cancellable';
|
||||
|
||||
Tezos.setProvider({
|
||||
@ -32,8 +33,10 @@ export class DeployAction extends CancellableAction {
|
||||
}
|
||||
|
||||
dispatch({ ...new UpdateLoadingAction('Compiling storage...') });
|
||||
const michelsonStorage = await compileExpression(
|
||||
const michelsonStorage = await compileStorage(
|
||||
editorState.language,
|
||||
editorState.code,
|
||||
deployState.entrypoint,
|
||||
deployState.storage,
|
||||
MichelsonFormat.Json
|
||||
);
|
||||
@ -83,13 +86,18 @@ export class DeployAction extends CancellableAction {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ ...new ChangeContractAction(contract.address) });
|
||||
dispatch({
|
||||
...new ChangeContractAction(contract.address, Command.Deploy)
|
||||
});
|
||||
} catch (ex) {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
|
||||
...new ChangeOutputAction(
|
||||
`Error: ${getErrorMessage(ex)}`,
|
||||
Command.Deploy
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { dryRun, getErrorMessage } from '../../services/api';
|
||||
import { AppState } from '../app';
|
||||
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
|
||||
import { ChangeOutputAction } from '../result';
|
||||
import { Command } from '../types';
|
||||
import { CancellableAction } from './cancellable';
|
||||
|
||||
export class DryRunAction extends CancellableAction {
|
||||
@ -25,13 +26,16 @@ export class DryRunAction extends CancellableAction {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({ ...new ChangeOutputAction(result.output) });
|
||||
dispatch({ ...new ChangeOutputAction(result.output, Command.DryRun) });
|
||||
} catch (ex) {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
|
||||
...new ChangeOutputAction(
|
||||
`Error: ${getErrorMessage(ex)}`,
|
||||
Command.DryRun
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { getErrorMessage, runFunction } from '../../services/api';
|
||||
import { AppState } from '../app';
|
||||
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
|
||||
import { ChangeOutputAction } from '../result';
|
||||
import { Command } from '../types';
|
||||
import { CancellableAction } from './cancellable';
|
||||
|
||||
export class EvaluateFunctionAction extends CancellableAction {
|
||||
@ -27,13 +28,18 @@ export class EvaluateFunctionAction extends CancellableAction {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({ ...new ChangeOutputAction(result.output) });
|
||||
dispatch({
|
||||
...new ChangeOutputAction(result.output, Command.EvaluateFunction)
|
||||
});
|
||||
} catch (ex) {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
|
||||
...new ChangeOutputAction(
|
||||
`Error: ${getErrorMessage(ex)}`,
|
||||
Command.EvaluateFunction
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { evaluateValue, getErrorMessage } from '../../services/api';
|
||||
import { AppState } from '../app';
|
||||
import { DoneLoadingAction, UpdateLoadingAction } from '../loading';
|
||||
import { ChangeOutputAction } from '../result';
|
||||
import { Command } from '../types';
|
||||
import { CancellableAction } from './cancellable';
|
||||
|
||||
export class EvaluateValueAction extends CancellableAction {
|
||||
@ -28,13 +29,18 @@ export class EvaluateValueAction extends CancellableAction {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ ...new ChangeOutputAction(result.code) });
|
||||
dispatch({
|
||||
...new ChangeOutputAction(result.code, Command.EvaluateValue)
|
||||
});
|
||||
} catch (ex) {
|
||||
if (this.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
dispatch({
|
||||
...new ChangeOutputAction(`Error: ${getErrorMessage(ex)}`)
|
||||
...new ChangeOutputAction(
|
||||
`Error: ${getErrorMessage(ex)}`,
|
||||
Command.EvaluateValue
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,36 @@
|
||||
import { Command } from './types';
|
||||
|
||||
export enum ActionType {
|
||||
ChangeOutput = 'result-change-output',
|
||||
ChangeContract = 'result-change-contract'
|
||||
}
|
||||
|
||||
export interface ResultState {
|
||||
command: Command;
|
||||
output: string;
|
||||
contract: string;
|
||||
}
|
||||
|
||||
export class ChangeOutputAction {
|
||||
public readonly type = ActionType.ChangeOutput;
|
||||
constructor(public payload: ResultState['output']) {}
|
||||
constructor(
|
||||
public output: ResultState['output'],
|
||||
public command: ResultState['command']
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ChangeContractAction {
|
||||
public readonly type = ActionType.ChangeContract;
|
||||
constructor(public payload: ResultState['contract']) {}
|
||||
constructor(
|
||||
public contract: ResultState['contract'],
|
||||
public command: ResultState['command']
|
||||
) {}
|
||||
}
|
||||
|
||||
type Action = ChangeOutputAction | ChangeContractAction;
|
||||
|
||||
const DEFAULT_STATE: ResultState = {
|
||||
command: Command.Compile,
|
||||
output: '',
|
||||
contract: ''
|
||||
};
|
||||
@ -30,13 +40,15 @@ export default (state = DEFAULT_STATE, action: Action): ResultState => {
|
||||
case ActionType.ChangeOutput:
|
||||
return {
|
||||
...state,
|
||||
output: action.payload
|
||||
output: action.output,
|
||||
command: action.command
|
||||
};
|
||||
case ActionType.ChangeContract:
|
||||
return {
|
||||
...state,
|
||||
output: DEFAULT_STATE.output,
|
||||
contract: action.payload
|
||||
contract: action.contract,
|
||||
command: action.command
|
||||
};
|
||||
}
|
||||
return state;
|
||||
|
@ -36,6 +36,23 @@ export async function compileExpression(
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function compileStorage(
|
||||
syntax: Language,
|
||||
code: string,
|
||||
entrypoint: string,
|
||||
storage: string,
|
||||
format?: string
|
||||
) {
|
||||
const response = await axios.post('/api/compile-storage', {
|
||||
syntax,
|
||||
code,
|
||||
entrypoint,
|
||||
storage,
|
||||
format
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function dryRun(
|
||||
syntax: Language,
|
||||
code: string,
|
||||
|
@ -79,6 +79,19 @@ exports.verifyAllExamples = async (action, done) => {
|
||||
done();
|
||||
};
|
||||
|
||||
exports.verifyWithParameter = async (command, parameter, value, action, done) => {
|
||||
await page.click('#command-select');
|
||||
await page.click(`#${command}`);
|
||||
|
||||
await page.click(`#${parameter}`);
|
||||
await exports.clearText(page.keyboard);
|
||||
await page.keyboard.type(value);
|
||||
|
||||
expect(await action()).toEqual(`Error: "${parameter}" is not allowed to be empty`);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
exports.verifyWithBlankParameter = async (command, parameter, action, done) => {
|
||||
await page.click('#command-select');
|
||||
await page.click(`#${command}`);
|
||||
|
38
tools/webide/packages/e2e/test/deploy.spec.js
Normal file
38
tools/webide/packages/e2e/test/deploy.spec.js
Normal file
@ -0,0 +1,38 @@
|
||||
const commonUtils = require('./common-utils');
|
||||
|
||||
const API_HOST = commonUtils.API_HOST;
|
||||
|
||||
const runCommandAndGetOutputFor = commonUtils.runCommandAndGetOutputFor;
|
||||
const clearText = commonUtils.clearText;
|
||||
|
||||
const COMMAND = 'deploy';
|
||||
const COMMAND_ENDPOINT = 'deploy';
|
||||
|
||||
async function deploy() {
|
||||
return await runCommandAndGetOutputFor(COMMAND, COMMAND_ENDPOINT);
|
||||
}
|
||||
|
||||
describe('Deploy contract', () => {
|
||||
beforeAll(() => jest.setTimeout(60000));
|
||||
|
||||
beforeEach(async () => await page.goto(API_HOST));
|
||||
|
||||
it('should deploy', async done => {
|
||||
expect(await deploy()).toContain('The contract was successfully deployed to the babylonnet test network.');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fail to deploy contract with invalid storage', async done => {
|
||||
await page.click('#command-select');
|
||||
await page.click(`#deploy`);
|
||||
|
||||
await page.click(`#storage`);
|
||||
await clearText(page.keyboard);
|
||||
await page.keyboard.type('asdf');
|
||||
|
||||
expect(await deploy()).toContain('Error: ');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
@ -40,10 +40,12 @@ export async function deployHandler(req: Request, res: Response) {
|
||||
'json'
|
||||
);
|
||||
|
||||
const michelsonStorage = await new LigoCompiler().compileExpression(
|
||||
const michelsonStorage = await new LigoCompiler().compileStorage(
|
||||
body.syntax,
|
||||
body.storage,
|
||||
'json'
|
||||
body.code,
|
||||
body.entrypoint,
|
||||
'json',
|
||||
body.storage
|
||||
);
|
||||
|
||||
await Tezos.importKey(await fetchRandomPrivateKey());
|
||||
|
@ -120,6 +120,7 @@ export class LigoCompiler {
|
||||
format: string
|
||||
) {
|
||||
const { name, remove } = await this.createTemporaryFile(code);
|
||||
|
||||
try {
|
||||
const result = await this.execPromise(this.ligoCmd, [
|
||||
'compile-contract',
|
||||
@ -148,6 +149,33 @@ export class LigoCompiler {
|
||||
return result;
|
||||
}
|
||||
|
||||
async compileStorage(
|
||||
syntax: string,
|
||||
code: string,
|
||||
entrypoint: string,
|
||||
format: string,
|
||||
storage: string
|
||||
) {
|
||||
const { name, remove } = await this.createTemporaryFile(code);
|
||||
|
||||
try {
|
||||
const result = await this.execPromise(this.ligoCmd, [
|
||||
'compile-storage',
|
||||
'--michelson-format',
|
||||
format,
|
||||
'-s',
|
||||
syntax,
|
||||
name,
|
||||
entrypoint,
|
||||
storage
|
||||
]);
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
remove();
|
||||
}
|
||||
}
|
||||
|
||||
async dryRun(
|
||||
syntax: string,
|
||||
code: string,
|
||||
|
37
tools/webide/packages/server/test/ligo-compiler.spec.ts
Normal file
37
tools/webide/packages/server/test/ligo-compiler.spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { LigoCompiler } from '../src/ligo-compiler';
|
||||
|
||||
const PASCALIGO_CODE = `
|
||||
type action is
|
||||
| Increment of int
|
||||
| Decrement of int
|
||||
|
||||
function add (const a : int ; const b : int) : int is
|
||||
block { skip } with a + b
|
||||
|
||||
function subtract (const a : int ; const b : int) : int is
|
||||
block { skip } with a - b
|
||||
|
||||
function main (const p : action ; const s : int) :
|
||||
(list(operation) * int) is
|
||||
block { skip } with ((nil : list(operation)),
|
||||
case p of
|
||||
| Increment(n) -> add(s, n)
|
||||
| Decrement(n) -> subtract(s, n)
|
||||
end)
|
||||
`;
|
||||
|
||||
describe('Ligo compiler', () => {
|
||||
it('should compile storage', async done => {
|
||||
const michelsonCode = await new LigoCompiler().compileStorage(
|
||||
'pascaligo',
|
||||
PASCALIGO_CODE,
|
||||
'main',
|
||||
'json',
|
||||
'0'
|
||||
);
|
||||
|
||||
expect(michelsonCode.trim()).toEqual('{ "int": "0" }');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user