mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
2 Commits
v2.4.2
...
suggestion
| Author | SHA1 | Date | |
|---|---|---|---|
| 9598c11f42 | |||
| 334a849caa |
209
example.html
209
example.html
@@ -1,107 +1,114 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="IE=edge" />
|
<meta charset='utf-8' http-equiv='X-UA-Compatible' content='IE=edge' />
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
|
<link rel='stylesheet' href='https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css' integrity='sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh' crossorigin='anonymous'>
|
||||||
<script src="https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1"></script>
|
<script src='https://cdn.jsdelivr.net/combine/npm/chart.js@2.9.3,npm/jquery@3.5.1,npm/@sasjs/adapter@1'></script>
|
||||||
<script>
|
<script>
|
||||||
var sasJs = new SASjs.default({appLoc: "/Products/demo/readme"
|
const sasJs = new SASjs.default({
|
||||||
,serverType:"SAS9", debug: "false"
|
appLoc: '/Products/demo/readme',
|
||||||
});
|
serverType:'SAS9',
|
||||||
function initSasJs() {
|
debug: 'false'
|
||||||
$('#loading-spinner').show()
|
})
|
||||||
// instantiate sasjs with options such as backend app location
|
|
||||||
// login (it's also possible to set an autologin when making requests)
|
const initSasJs = () => {
|
||||||
sasJs.logIn(
|
$('#loading-spinner').show()
|
||||||
$('#username')[0].value
|
|
||||||
,$('#password')[0].value
|
// instantiate sasJs with options such as backend app location
|
||||||
).then((response) => {
|
// login (it's also possible to set an auto login when making requests)
|
||||||
if (response.isLoggedIn === true) {
|
sasJs.logIn($('#username')[0].value, $('#password')[0].value)
|
||||||
$('#loading-spinner').hide()
|
.then((response) => {
|
||||||
$('.login').hide()
|
if (response.isLoggedIn === true) {
|
||||||
$('#getdata').show()
|
$('#loading-spinner').hide()
|
||||||
$('#cars').show()
|
$('.login').hide()
|
||||||
}
|
$('#getDataBtn').show()
|
||||||
})
|
$('#cars').show()
|
||||||
}
|
}
|
||||||
function getData(){
|
})
|
||||||
$('#loading-spinner').show()
|
|
||||||
$('#myChart').remove();
|
|
||||||
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
|
|
||||||
// make a request to a SAS service
|
|
||||||
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
|
|
||||||
// request data from an endpoint under your appLoc
|
|
||||||
sasJs.request("/common/getdata", {
|
|
||||||
// send data as an array of objects - each object is one row
|
|
||||||
fromjs: [{ type: type }]
|
|
||||||
}).then((response) => {
|
|
||||||
$('#myChart').show();
|
|
||||||
var labels = []
|
|
||||||
var data = []
|
|
||||||
response.areas.map((d) => {
|
|
||||||
labels.push(d.MAKE);
|
|
||||||
data.push(d.AVPRICE);
|
|
||||||
})
|
|
||||||
$('#loading-spinner').hide()
|
|
||||||
initGraph(labels, data, type);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
function initGraph(labels, data, type){
|
|
||||||
var myCanvas = document.getElementById("myChart");
|
|
||||||
var ctx = myCanvas.getContext("2d");
|
|
||||||
var myChart = new Chart(ctx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: "Average Invoice Price in USD for " + type + " Cars by Manufacturer",
|
|
||||||
data: data,
|
|
||||||
backgroundColor: "rgba(255,99,132,0.2)",
|
|
||||||
borderColor: "rgba(255,99,132,1)",
|
|
||||||
borderWidth: 1,
|
|
||||||
hoverBackgroundColor: "rgba(255,99,132,0.4)",
|
|
||||||
hoverBorderColor: "rgba(255,99,132,1)",
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {yAxes: [{ticks: {beginAtZero: true}}]}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
// make a request to a SAS service
|
||||||
</script>
|
const getData = () => {
|
||||||
</head>
|
$('#loading-spinner').show()
|
||||||
<body>
|
$('#myChart').remove()
|
||||||
<div class="container-fluid" style="text-align: center; margin-top: 10px;">
|
$('#chart-container').append("<canvas id='myChart' style='display: none'></canvas>")
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto">
|
const type = $('#cars')[0].options[$('#cars')[0].selectedIndex].value
|
||||||
<h1>Demo Seed App for <span class="code">SASjs</span></h1>
|
|
||||||
<div class="login" id="login-form">
|
// request data from an endpoint under your appLoc
|
||||||
<div class="form-group">
|
// send data as an array of objects - each object is one row
|
||||||
<input class="form-control" type="text" id="username" placeholder="Enter username" />
|
sasJs.request('/common/getdata', {fromjs: [{ type: type }]})
|
||||||
|
.then((response) => {
|
||||||
|
$('#myChart').show()
|
||||||
|
$('#loading-spinner').hide()
|
||||||
|
|
||||||
|
const labels = response.areas.map(area => area.MAKE)
|
||||||
|
const data = response.areas.map(area => area.AVPRICE)
|
||||||
|
|
||||||
|
initGraph(labels, data, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const initGraph = (labels, data, type) => {
|
||||||
|
const myCanvas = document.getElementById('myChart')
|
||||||
|
const ctx = myCanvas.getContext('2d')
|
||||||
|
|
||||||
|
const myChart = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: `Average Invoice Price in USD for ${type} Cars by Manufacturer`,
|
||||||
|
data: data,
|
||||||
|
backgroundColor: 'rgba(255,99,132,0.2)',
|
||||||
|
borderColor: 'rgba(255,99,132,1)',
|
||||||
|
borderWidth: 1,
|
||||||
|
hoverBackgroundColor: 'rgba(255,99,132,0.4)',
|
||||||
|
hoverBorderColor: 'rgba(255,99,132,1)',
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
scales: {yAxes: [{ticks: {beginAtZero: true}}]}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class='container-fluid' style='text-align: center; margin-top: 10px'>
|
||||||
|
<div class='row'>
|
||||||
|
<div class='col-lg-5 col-md-7 col-sm-10 mx-auto mx-auto'>
|
||||||
|
<h1>Demo Seed App for <span class='code'>SASjs</span></h1>
|
||||||
|
<div class='login' id='login-form'>
|
||||||
|
<div class='form-group'>
|
||||||
|
<input class='form-control' type='text' id='username' placeholder='Enter username' />
|
||||||
|
</div>
|
||||||
|
<div class='form-group'>
|
||||||
|
<input class='form-control' type='password' id='password' placeholder='Enter password' />
|
||||||
|
</div>
|
||||||
|
<button id='login' onclick='initSasJs()' class='login btn btn-primary' style='margin-bottom: 5px'>Log In</button>
|
||||||
|
</div>
|
||||||
|
<select name='cars' id='cars' style='margin-bottom: 5px; display: none' class='form-control'>
|
||||||
|
<option value='Hybrid'>Hybrid</option>
|
||||||
|
<option value='SUV'>SUV</option>
|
||||||
|
<option value='Sedan'>Sedan</option>
|
||||||
|
<option value='Sports'>Sports</option>
|
||||||
|
<option value='Truck'>Truck</option>
|
||||||
|
<option value='Wagon'>Wagon</option>
|
||||||
|
</select>
|
||||||
|
<button id='getDataBtn' onclick='getData()' style='margin-bottom: 5px; display: none' class='btn btn-success'>Get Data</button>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<div id='loading-spinner' class='spinner-border text-primary' role='status' style='display: none'>
|
||||||
|
<span class='sr-only'>Loading...</span>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<input class="form-control" type="password" id="password" placeholder="Enter password" />
|
|
||||||
</div>
|
|
||||||
<button id="login" onclick="initSasJs()" class="login btn btn-primary" style="margin-bottom: 5px;">Log In</button>
|
|
||||||
</div>
|
|
||||||
<select name="cars" id="cars" style="margin-bottom: 5px; display: none;" class="form-control">
|
|
||||||
<option value="Hybrid">Hybrid</option>
|
|
||||||
<option value="SUV">SUV</option>
|
|
||||||
<option value="Sedan">Sedan</option>
|
|
||||||
<option value="Sports">Sports</option>
|
|
||||||
<option value="Truck">Truck</option>
|
|
||||||
<option value="Wagon">Wagon</option>
|
|
||||||
</select>
|
|
||||||
<button id="getdata" onclick="getData()" style="margin-bottom: 5px; display: none;" class="btn btn-success">Get Data</button><br><br>
|
|
||||||
<div id="loading-spinner" class="spinner-border text-primary" role="status" style="display: none;">
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div><br>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id='chart-container' style='height: 65vh; width: 100%; position: relative; margin: auto'>
|
||||||
</div>
|
<canvas id='myChart' style='display: none'></canvas>
|
||||||
<div id="chart-container" style="height: 65vh; width: 100%; position: relative; margin: auto;">
|
</div>
|
||||||
<canvas id="myChart" style="display: none;"></canvas>
|
</body>
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</head>
|
</head>
|
||||||
@@ -44,4 +44,4 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"node-sass": "^4.14.1"
|
"node-sass": "^4.14.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,6 @@ import App from './App';
|
|||||||
test('renders learn react link', () => {
|
test('renders learn react link', () => {
|
||||||
const { getByText } = render(<App />);
|
const { getByText } = render(<App />);
|
||||||
const linkElement = getByText(/learn react/i);
|
const linkElement = getByText(/learn react/i);
|
||||||
|
|
||||||
expect(linkElement).toBeInTheDocument();
|
expect(linkElement).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ const App = (): ReactElement<{}> => {
|
|||||||
const { adapter } = useContext(AppContext);
|
const { adapter } = useContext(AppContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (adapter) {
|
if (adapter) adapter.setDebugState(debug);
|
||||||
adapter.setDebugState(debug);
|
|
||||||
}
|
|
||||||
}, [debug, adapter]);
|
}, [debug, adapter]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +31,7 @@ const App = (): ReactElement<{}> => {
|
|||||||
<label className="switch">
|
<label className="switch">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
onChange={(e) => setDebug(e.target.checked)}
|
onChange={(e) => setDebug(e.target.checked)} // FIXME: rename 'e' => 'event'
|
||||||
/>
|
/>
|
||||||
<span className="knob"></span>
|
<span className="knob"></span>
|
||||||
</label>
|
</label>
|
||||||
@@ -45,7 +43,7 @@ const App = (): ReactElement<{}> => {
|
|||||||
type="text"
|
type="text"
|
||||||
className="app-loc-input"
|
className="app-loc-input"
|
||||||
value={appLoc}
|
value={appLoc}
|
||||||
onChange={(e) => setAppLoc(e.target.value)}
|
onChange={(e) => setAppLoc(e.target.value)} // FIXME: rename 'e' => 'event'
|
||||||
placeholder="AppLoc"
|
placeholder="AppLoc"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,11 +9,14 @@ const Login = (): ReactElement<{}> => {
|
|||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
(e) => {
|
(e) => { // FIXME: rename 'e' => 'event'
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
appContext.adapter.logIn(username, password).then(() => {
|
|
||||||
appContext.setIsLoggedIn(true);
|
appContext.adapter.logIn(username, password)
|
||||||
});
|
.then(() => {
|
||||||
|
appContext.setIsLoggedIn(true);
|
||||||
|
});
|
||||||
|
// FIXME: catch block
|
||||||
},
|
},
|
||||||
[username, password, appContext]
|
[username, password, appContext]
|
||||||
);
|
);
|
||||||
@@ -38,7 +41,7 @@ const Login = (): ReactElement<{}> => {
|
|||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
required
|
required
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)} // FIXME: rename 'e' => 'event'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" className="submit-button">
|
<button type="submit" className="submit-button">
|
||||||
@@ -51,4 +54,4 @@ const Login = (): ReactElement<{}> => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
@@ -8,16 +8,14 @@ interface PrivateRouteProps {
|
|||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PrivateRoute = (
|
const PrivateRoute = (props: PrivateRouteProps): ReactElement<PrivateRouteProps> => {
|
||||||
props: PrivateRouteProps
|
|
||||||
): ReactElement<PrivateRouteProps> => {
|
|
||||||
const { component, path, exact } = props;
|
const { component, path, exact } = props;
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
return appContext.isLoggedIn ? (
|
|
||||||
|
return appContext.isLoggedIn ?
|
||||||
<Route component={component} path={path} exact={exact} />
|
<Route component={component} path={path} exact={exact} />
|
||||||
) : (
|
:
|
||||||
<Redirect to="/login" />
|
<Redirect to="/login" />
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PrivateRoute;
|
export default PrivateRoute;
|
||||||
@@ -2,25 +2,24 @@ import React, { useEffect, useState, ReactElement, useContext } from "react";
|
|||||||
import TestSuiteComponent from "./components/TestSuite";
|
import TestSuiteComponent from "./components/TestSuite";
|
||||||
import TestSuiteCard from "./components/TestSuiteCard";
|
import TestSuiteCard from "./components/TestSuiteCard";
|
||||||
import { TestSuite, Test } from "./types";
|
import { TestSuite, Test } from "./types";
|
||||||
import { basicTests } from "./testSuites/Basic";
|
import { basicTests } from "./testSuites/Basic"; // FIXME: declared but never used
|
||||||
import "./TestSuiteRunner.scss";
|
import "./TestSuiteRunner.scss";
|
||||||
import SASjs from "sasjs";
|
import SASjs from "sasjs";
|
||||||
import { AppContext } from "./context/AppContext";
|
import { AppContext } from "./context/AppContext";
|
||||||
import { sendArrTests, sendObjTests } from "./testSuites/RequestData";
|
import { sendArrTests, sendObjTests } from "./testSuites/RequestData"; // FIXME: declared but never used
|
||||||
import { specialCaseTests } from "./testSuites/SpecialCases";
|
import { specialCaseTests } from "./testSuites/SpecialCases";
|
||||||
import { sasjsRequestTests } from "./testSuites/SasjsRequests";
|
import { sasjsRequestTests } from "./testSuites/SasjsRequests"; // FIXME: declared but never used
|
||||||
|
|
||||||
interface TestSuiteRunnerProps {
|
interface TestSuiteRunnerProps {
|
||||||
adapter: SASjs;
|
adapter: SASjs;
|
||||||
}
|
}
|
||||||
const TestSuiteRunner = (
|
|
||||||
props: TestSuiteRunnerProps
|
const TestSuiteRunner = (props: TestSuiteRunnerProps): ReactElement<TestSuiteRunnerProps> => {
|
||||||
): ReactElement<TestSuiteRunnerProps> => {
|
|
||||||
const { adapter } = props;
|
const { adapter } = props;
|
||||||
const { config } = useContext(AppContext);
|
const { config } = useContext(AppContext); // FIXME: declared but never used
|
||||||
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
const [testSuites, setTestSuites] = useState<TestSuite[]>([]);
|
||||||
const [runTests, setRunTests] = useState(false);
|
const [runTests, setRunTests] = useState(false);
|
||||||
const [completedTestSuites, setCompletedTestSuites] = useState<
|
const [completedTestSuites, setCompletedTestSuites] = useState< // FIXME: create interface
|
||||||
{
|
{
|
||||||
name: string;
|
name: string;
|
||||||
completedTests: {
|
completedTests: {
|
||||||
@@ -65,20 +64,23 @@ const TestSuiteRunner = (
|
|||||||
<>
|
<>
|
||||||
<div className="button-container">
|
<div className="button-container">
|
||||||
<button
|
<button
|
||||||
className={runTests ? "submit-button disabled" : "submit-button"}
|
className={runTests ? "submit-button disabled" : "submit-button"} // TODO: 'submit-button' class should be assigned by default
|
||||||
onClick={() => setRunTests(true)}
|
onClick={() => setRunTests(true)}
|
||||||
disabled={runTests}
|
disabled={runTests}
|
||||||
>
|
>
|
||||||
{runTests ? (
|
{runTests ? (
|
||||||
<>
|
<>
|
||||||
<div className="loading-spinner"></div>Running tests...
|
{
|
||||||
|
// FIXME: fragment is not needed in this case
|
||||||
|
}
|
||||||
|
<div className="loading-spinner"></div>Running tests...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
"Run tests!"
|
"Run tests!"
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{completedTestSuites.map((completedTestSuite, index) => {
|
{completedTestSuites.map((completedTestSuite, index) => { // TODO: refactor
|
||||||
return (
|
return (
|
||||||
<TestSuiteCard
|
<TestSuiteCard
|
||||||
key={index}
|
key={index}
|
||||||
@@ -100,22 +102,19 @@ const TestSuiteRunner = (
|
|||||||
}[]
|
}[]
|
||||||
) => {
|
) => {
|
||||||
const currentIndex = testSuites.indexOf(currentTestSuite);
|
const currentIndex = testSuites.indexOf(currentTestSuite);
|
||||||
const nextIndex =
|
const nextIndex = currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
|
||||||
currentIndex < testSuites.length - 1 ? currentIndex + 1 : -1;
|
|
||||||
if (nextIndex >= 0) {
|
if (nextIndex >= 0) setCurrentTestSuite(testSuites[nextIndex]);
|
||||||
setCurrentTestSuite(testSuites[nextIndex]);
|
else setCurrentTestSuite(null);
|
||||||
} else {
|
|
||||||
setCurrentTestSuite(null);
|
|
||||||
}
|
|
||||||
const newCompletedTestSuites = [
|
const newCompletedTestSuites = [
|
||||||
...completedTestSuites,
|
...completedTestSuites,
|
||||||
{ name, completedTests },
|
{ name, completedTests },
|
||||||
];
|
];
|
||||||
|
|
||||||
setCompletedTestSuites(newCompletedTestSuites);
|
setCompletedTestSuites(newCompletedTestSuites);
|
||||||
|
|
||||||
if (newCompletedTestSuites.length === testSuites.length) {
|
if (newCompletedTestSuites.length === testSuites.length) setRunTests(false);
|
||||||
setRunTests(false);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -123,4 +122,4 @@ const TestSuiteRunner = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TestSuiteRunner;
|
export default TestSuiteRunner;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from "react";
|
import React, { ReactElement, useEffect, useState } from "react";
|
||||||
import TestCard from "./TestCard";
|
import TestCard from "./TestCard";
|
||||||
import { start } from "repl";
|
import { start } from "repl"; // FIXME: declared but never used
|
||||||
|
|
||||||
interface TestProps {
|
interface TestProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -39,28 +39,36 @@ const Test = (props: TestProps): ReactElement<TestProps> => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (test && assertion) {
|
if (test && assertion) {
|
||||||
const startTime = new Date().valueOf();
|
const startTime = new Date().valueOf()
|
||||||
|
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
setIsPassed(false);
|
setIsPassed(false);
|
||||||
|
|
||||||
beforeTestFunction()
|
beforeTestFunction()
|
||||||
.then(() => test(context))
|
.then(() => test(context))
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
setIsPassed(assertion(res, context));
|
setIsPassed(assertion(res, context))
|
||||||
|
|
||||||
return Promise.resolve(assertion(res, context));
|
return Promise.resolve(assertion(res, context));
|
||||||
})
|
})
|
||||||
.then((testResult) => {
|
.then((testResult) => {
|
||||||
afterTestFunction();
|
afterTestFunction();
|
||||||
|
|
||||||
const endTime = new Date().valueOf();
|
const endTime = new Date().valueOf();
|
||||||
const executionTime = (endTime - startTime) / 1000;
|
const executionTime = (endTime - startTime) / 1000;
|
||||||
|
|
||||||
onCompleted({ result: testResult, error: null, executionTime });
|
onCompleted({ result: testResult, error: null, executionTime });
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
setIsPassed(false);
|
setIsPassed(false);
|
||||||
|
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
const endTime = new Date().valueOf();
|
const endTime = new Date().valueOf();
|
||||||
const executionTime = (endTime - startTime) / 1000;
|
const executionTime = (endTime - startTime) / 1000;
|
||||||
|
|
||||||
onCompleted({ result: false, error: e, executionTime });
|
onCompleted({ result: false, error: e, executionTime });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -76,4 +84,4 @@ const Test = (props: TestProps): ReactElement<TestProps> => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Test;
|
export default Test;
|
||||||
@@ -18,7 +18,7 @@ const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
|
|||||||
<span className="execution-time">
|
<span className="execution-time">
|
||||||
{executionTime ? executionTime.toFixed(2) + "s" : ""}
|
{executionTime ? executionTime.toFixed(2) + "s" : ""}
|
||||||
</span>
|
</span>
|
||||||
{status === "running" && (
|
{status === "running" && ( // FIXME: use switch statement
|
||||||
<div>
|
<div>
|
||||||
<span className="icon running"></span>Running...
|
<span className="icon running"></span>Running...
|
||||||
</div>
|
</div>
|
||||||
@@ -40,4 +40,4 @@ const TestCard = (props: TestCardProps): ReactElement<TestCardProps> => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TestCard;
|
export default TestCard;
|
||||||
@@ -22,7 +22,7 @@ interface TestSuiteProps {
|
|||||||
const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
||||||
const { name, tests, beforeAll, afterAll, onCompleted } = props;
|
const { name, tests, beforeAll, afterAll, onCompleted } = props;
|
||||||
const [context, setContext] = useState<any>(null);
|
const [context, setContext] = useState<any>(null);
|
||||||
const [completedTests, setCompletedTests] = useState<
|
const [completedTests, setCompletedTests] = useState< // TODO: create an interface
|
||||||
{
|
{
|
||||||
test: Test;
|
test: Test;
|
||||||
result: boolean;
|
result: boolean;
|
||||||
@@ -31,24 +31,21 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
|||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
const [currentTest, setCurrentTest] = useState<Test | null>(
|
const [currentTest, setCurrentTest] = useState<Test | null>(
|
||||||
(null as unknown) as Test
|
(null as unknown) as Test // ?
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (beforeAll) {
|
if (beforeAll) beforeAll().then((data) => setContext({ data }))
|
||||||
beforeAll().then((data) => setContext({ data }));
|
|
||||||
}
|
|
||||||
}, [beforeAll]);
|
}, [beforeAll]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tests.length) {
|
if (tests.length) setCurrentTest(tests[0])
|
||||||
setCurrentTest(tests[0]);
|
|
||||||
}
|
|
||||||
setCompletedTests([]);
|
setCompletedTests([]);
|
||||||
setContext(null);
|
setContext(null);
|
||||||
}, [tests]);
|
}, [tests]);
|
||||||
|
|
||||||
return (!!beforeAll && !!context) || !beforeAll ? (
|
return (!!beforeAll && !!context) || !beforeAll ? ( // ?
|
||||||
<div className="test-suite">
|
<div className="test-suite">
|
||||||
<div className="test-suite-name running">{name}</div>
|
<div className="test-suite-name running">{name}</div>
|
||||||
{currentTest && (
|
{currentTest && (
|
||||||
@@ -64,29 +61,27 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
|||||||
error: completedTest.error,
|
error: completedTest.error,
|
||||||
executionTime: completedTest.executionTime,
|
executionTime: completedTest.executionTime,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
||||||
setCompletedTests(newCompleteTests);
|
setCompletedTests(newCompleteTests);
|
||||||
|
|
||||||
const currentIndex = tests.indexOf(currentTest);
|
const currentIndex = tests.indexOf(currentTest);
|
||||||
const nextIndex =
|
const nextIndex = currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
|
||||||
currentIndex < tests.length - 1 ? currentIndex + 1 : -1;
|
|
||||||
if (nextIndex >= 0) {
|
if (nextIndex >= 0) setCurrentTest(tests[nextIndex]);
|
||||||
setCurrentTest(tests[nextIndex]);
|
else setCurrentTest(null);
|
||||||
} else {
|
|
||||||
setCurrentTest(null);
|
|
||||||
}
|
|
||||||
if (newCompleteTests.length === tests.length) {
|
if (newCompleteTests.length === tests.length) {
|
||||||
if (afterAll) {
|
if (afterAll) afterAll().then(() => onCompleted(name, newCompleteTests))
|
||||||
afterAll().then(() => onCompleted(name, newCompleteTests));
|
else onCompleted(name, newCompleteTests)
|
||||||
} else {
|
|
||||||
onCompleted(name, newCompleteTests);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{completedTests.map((completedTest, index) => {
|
{completedTests.map((test, index) => {
|
||||||
const { test, result, error } = completedTest;
|
const { test, result, error } = test;
|
||||||
const { title, description } = test;
|
const { title, description } = test;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestCard
|
<TestCard
|
||||||
key={index}
|
key={index}
|
||||||
@@ -99,8 +94,8 @@ const TestSuite = (props: TestSuiteProps): ReactElement<TestSuiteProps> => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></> // FIXME: use {null} instead
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TestSuite;
|
export default TestSuite;
|
||||||
@@ -12,20 +12,19 @@ interface TestSuiteCardProps {
|
|||||||
executionTime: number;
|
executionTime: number;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
const TestSuiteCard = (
|
const TestSuiteCard = (props: TestSuiteCardProps): ReactElement<TestSuiteCardProps> => {
|
||||||
props: TestSuiteCardProps
|
|
||||||
): ReactElement<TestSuiteCardProps> => {
|
|
||||||
const { name, tests } = props;
|
const { name, tests } = props;
|
||||||
const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y);
|
const overallStatus = tests.map((t) => t.result).reduce((x, y) => x && y); // TODO: refactor variable names
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="test-suite">
|
<div className="test-suite">
|
||||||
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
|
<div className={`test-suite-name ${overallStatus ? "passed" : "failed"}`}>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
{tests.map((completedTest, index) => {
|
{tests.map((test, index) => {
|
||||||
const { test, result, error, executionTime } = completedTest;
|
const { test, result, error, executionTime } = test;
|
||||||
const { title, description } = test;
|
const { title, description } = test;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TestCard
|
<TestCard
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { createContext, useState, useEffect, ReactNode } from "react";
|
import React, { createContext, useState, useEffect, ReactNode } from "react";
|
||||||
import SASjs from "sasjs";
|
import SASjs from "sasjs";
|
||||||
|
|
||||||
export const AppContext = createContext<{
|
export const AppContext = createContext<{ // TODO: create an interface
|
||||||
config: any;
|
config: any; // TODO: be more specific on type declaration
|
||||||
sasJsConfig: any;
|
sasJsConfig: any; // TODO: be more specific on type declaration
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
setIsLoggedIn: (value: boolean) => void;
|
setIsLoggedIn: (value: boolean) => void;
|
||||||
adapter: SASjs;
|
adapter: SASjs;
|
||||||
@@ -16,25 +16,24 @@ export const AppContext = createContext<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const AppProvider = (props: { children: ReactNode }) => {
|
export const AppProvider = (props: { children: ReactNode }) => {
|
||||||
const [config, setConfig] = useState<{ sasJsConfig: any }>({
|
const [config, setConfig] = useState<{ sasJsConfig: any }>({sasJsConfig: null}); // TODO: be more specific on type declaration
|
||||||
sasJsConfig: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
|
const [adapter, setAdapter] = useState<SASjs>((null as unknown) as SASjs);
|
||||||
|
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("config.json")
|
fetch("config.json") // TODO: use axios instead of fetch
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((configJson: any) => {
|
.then((configJson: any) => { // TODO: be more specific on type declaration
|
||||||
setConfig(configJson);
|
setConfig(configJson);
|
||||||
|
|
||||||
const sasjs = new SASjs(configJson.sasJsConfig);
|
const sasjs = new SASjs(configJson.sasJsConfig);
|
||||||
|
|
||||||
setAdapter(sasjs);
|
setAdapter(sasjs);
|
||||||
|
|
||||||
sasjs.checkSession().then((response) => {
|
sasjs.checkSession().then((response) => {
|
||||||
setIsLoggedIn(response.isLoggedIn);
|
setIsLoggedIn(response.isLoggedIn);
|
||||||
});
|
}); // FIXME: add catch block
|
||||||
});
|
});// FIXME: add catch block
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -50,4 +49,4 @@ export const AppProvider = (props: { children: ReactNode }) => {
|
|||||||
{props.children}
|
{props.children}
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -33,18 +33,16 @@ export const basicTests = (
|
|||||||
test: async () => {
|
test: async () => {
|
||||||
return adapter.logIn(userName, password);
|
return adapter.logIn(userName, password);
|
||||||
},
|
},
|
||||||
assertion: (response: any) =>
|
assertion: (response: any) => // FIXME: be more specific on type declaration
|
||||||
response && response.isLoggedIn && response.userName === userName,
|
response && response.isLoggedIn && response.userName === userName,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Default config",
|
title: "Default config",
|
||||||
description:
|
description: "Should instantiate with default config when none is provided",
|
||||||
"Should instantiate with default config when none is provided",
|
test: async () => Promise.resolve(new SASjs()),
|
||||||
test: async () => {
|
|
||||||
return Promise.resolve(new SASjs());
|
|
||||||
},
|
|
||||||
assertion: (sasjsInstance: SASjs) => {
|
assertion: (sasjsInstance: SASjs) => {
|
||||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "sasjs";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "../types";
|
||||||
|
|
||||||
const stringData: any = { table1: [{ col1: "first col value" }] };
|
const stringData: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
|
||||||
const numericData: any = { table1: [{ col1: 3.14159265 }] };
|
const numericData: any = { table1: [{ col1: 3.14159265 }] }; // TODO: be more specific on type declaration
|
||||||
const multiColumnData: any = {
|
const multiColumnData: any = { // TODO: be more specific on type declaration
|
||||||
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }],
|
table1: [{ col1: 42, col2: 1.618, col3: "x", col4: "x" }],
|
||||||
};
|
};
|
||||||
const multipleRowsWithNulls: any = {
|
const multipleRowsWithNulls: any = { // TODO: be more specific on type declaration
|
||||||
table1: [
|
table1: [
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: "" },
|
{ col1: 42, col2: null, col3: "x", col4: "" },
|
||||||
@@ -15,7 +15,7 @@ const multipleRowsWithNulls: any = {
|
|||||||
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
{ col1: 42, col2: 1.62, col3: "x", col4: "x" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const multipleColumnsWithNulls: any = {
|
const multipleColumnsWithNulls: any = { // TODO: be more specific on type declaration
|
||||||
table1: [
|
table1: [
|
||||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||||
{ col1: 42, col2: null, col3: "x", col4: null },
|
{ col1: 42, col2: null, col3: "x", col4: null },
|
||||||
@@ -25,21 +25,20 @@ const multipleColumnsWithNulls: any = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLongStringData = (length = 32764) => {
|
const getLongStringData = (length = 32764) => { // FIXME: add type declaration
|
||||||
let x = "X";
|
let x = "X";
|
||||||
for (let i = 1; i <= length; i++) {
|
|
||||||
x = x + "X";
|
for (let i = 1; i <= length; i++) x += 'X'
|
||||||
}
|
|
||||||
const data: any = { table1: [{ col1: x }] };
|
const data: any = { table1: [{ col1: x }] }; // TODO: be more specific on type declaration
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLargeObjectData = () => {
|
const getLargeObjectData = () => {
|
||||||
const data = { table1: [{ big: "data" }] };
|
const data = { table1: [{ big: "data" }] };
|
||||||
|
|
||||||
for (let i = 1; i < 10000; i++) {
|
for (let i = 1; i < 10000; i++) data.table1.push(data.table1[0])
|
||||||
data.table1.push(data.table1[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
@@ -50,12 +49,8 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
{
|
{
|
||||||
title: "Single string value",
|
title: "Single string value",
|
||||||
description: "Should send an array with a single string value",
|
description: "Should send an array with a single string value",
|
||||||
test: () => {
|
test: () => adapter.request("common/sendArr", stringData),
|
||||||
return adapter.request("common/sendArr", stringData);
|
assertion: (res: any) => res.table1[0][0] === stringData.table1[0].col1 // TODO: be more specific on type declaration
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
|
||||||
return res.table1[0][0] === stringData.table1[0].col1;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Long string value",
|
title: "Long string value",
|
||||||
@@ -64,22 +59,17 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", getLongStringData());
|
return adapter.request("common/sendArr", getLongStringData());
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => res.table1[0][0] === getLongStringData().table1[0].col1 // TODO: be more specific on type declaration
|
||||||
const longStringData = getLongStringData();
|
|
||||||
return res.table1[0][0] === longStringData.table1[0].col1;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Overly long string value",
|
title: "Overly long string value",
|
||||||
description:
|
description:
|
||||||
"Should error out with long string values over 32765 characters",
|
"Should error out with long string values over 32765 characters",
|
||||||
test: () => {
|
test: () => adapter
|
||||||
return adapter
|
.request("common/sendArr", getLongStringData(32767))
|
||||||
.request("common/sendArr", getLongStringData(32767))
|
.catch((e) => e), // TODO: rename
|
||||||
.catch((e) => e);
|
assertion: (error: any) => { // TODO: be more specific on type declaration
|
||||||
},
|
return !!error && !!error.MESSAGE; // FIXME: refactor
|
||||||
assertion: (error: any) => {
|
|
||||||
return !!error && !!error.MESSAGE;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,7 +78,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", numericData);
|
return adapter.request("common/sendArr", numericData);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
return res.table1[0][0] === numericData.table1[0].col1;
|
return res.table1[0][0] === numericData.table1[0].col1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -98,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", multiColumnData);
|
return adapter.request("common/sendArr", multiColumnData);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
return (
|
return (
|
||||||
res.table1[0][0] === multiColumnData.table1[0].col1 &&
|
res.table1[0][0] === multiColumnData.table1[0].col1 &&
|
||||||
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
res.table1[0][1] === multiColumnData.table1[0].col2 &&
|
||||||
@@ -113,9 +103,10 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", multipleRowsWithNulls);
|
return adapter.request("common/sendArr", multipleRowsWithNulls);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
let result = true;
|
let result = true;
|
||||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||||
|
// FIXME: use loop
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
|
res.table1[index][0] === multipleRowsWithNulls.table1[index].col1;
|
||||||
@@ -129,6 +120,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
result &&
|
result &&
|
||||||
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
|
res.table1[index][3] === multipleRowsWithNulls.table1[index].col4;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -138,9 +130,9 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendArr", multipleColumnsWithNulls);
|
return adapter.request("common/sendArr", multipleColumnsWithNulls);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
let result = true;
|
let result = true;
|
||||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index][0] ===
|
res.table1[index][0] ===
|
||||||
@@ -171,12 +163,12 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
title: "Invalid column name",
|
title: "Invalid column name",
|
||||||
description: "Should throw an error",
|
description: "Should throw an error",
|
||||||
test: async () => {
|
test: async () => {
|
||||||
const invalidData: any = {
|
const invalidData: any = { // TODO: be more specific on type declaration
|
||||||
"1 invalid table": [{ col1: 42 }],
|
"1 invalid table": [{ col1: 42 }],
|
||||||
};
|
};
|
||||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => !!error && !!error.MESSAGE,
|
assertion: (error: any) => !!error && !!error.MESSAGE, // TODO: be more specific on type declaration
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Single string value",
|
title: "Single string value",
|
||||||
@@ -184,7 +176,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", stringData);
|
return adapter.request("common/sendObj", stringData);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
return res.table1[0].COL1 === stringData.table1[0].col1;
|
return res.table1[0].COL1 === stringData.table1[0].col1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -195,7 +187,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getLongStringData());
|
return adapter.request("common/sendObj", getLongStringData());
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
const longStringData = getLongStringData();
|
const longStringData = getLongStringData();
|
||||||
return res.table1[0].COL1 === longStringData.table1[0].col1;
|
return res.table1[0].COL1 === longStringData.table1[0].col1;
|
||||||
},
|
},
|
||||||
@@ -209,7 +201,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
.request("common/sendObj", getLongStringData(32767))
|
.request("common/sendObj", getLongStringData(32767))
|
||||||
.catch((e) => e);
|
.catch((e) => e);
|
||||||
},
|
},
|
||||||
assertion: (error: any) => {
|
assertion: (error: any) => { // TODO: be more specific on type declaration
|
||||||
return !!error && !!error.MESSAGE;
|
return !!error && !!error.MESSAGE;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -219,7 +211,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", numericData);
|
return adapter.request("common/sendObj", numericData);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
return res.table1[0].COL1 === numericData.table1[0].col1;
|
return res.table1[0].COL1 === numericData.table1[0].col1;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -230,7 +222,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", getLargeObjectData());
|
return adapter.request("common/sendObj", getLargeObjectData());
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
const data = getLargeObjectData();
|
const data = getLargeObjectData();
|
||||||
return res.table1[9000].BIG === data.table1[9000].big;
|
return res.table1[9000].BIG === data.table1[9000].big;
|
||||||
},
|
},
|
||||||
@@ -241,7 +233,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multiColumnData);
|
return adapter.request("common/sendObj", multiColumnData);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
return (
|
return (
|
||||||
res.table1[0].COL1 === multiColumnData.table1[0].col1 &&
|
res.table1[0].COL1 === multiColumnData.table1[0].col1 &&
|
||||||
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
res.table1[0].COL2 === multiColumnData.table1[0].col2 &&
|
||||||
@@ -256,9 +248,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multipleRowsWithNulls);
|
return adapter.request("common/sendObj", multipleRowsWithNulls);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
let result = true;
|
let result = true;
|
||||||
multipleRowsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleRowsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
|
res.table1[index].COL1 === multipleRowsWithNulls.table1[index].col1;
|
||||||
@@ -281,9 +273,9 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
test: () => {
|
test: () => {
|
||||||
return adapter.request("common/sendObj", multipleColumnsWithNulls);
|
return adapter.request("common/sendObj", multipleColumnsWithNulls);
|
||||||
},
|
},
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => { // TODO: be more specific on type declaration
|
||||||
let result = true;
|
let result = true;
|
||||||
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => {
|
multipleColumnsWithNulls.table1.forEach((_: any, index: number) => { // TODO: be more specific on type declaration
|
||||||
result =
|
result =
|
||||||
result &&
|
result &&
|
||||||
res.table1[index].COL1 ===
|
res.table1[index].COL1 ===
|
||||||
@@ -305,4 +297,4 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "sasjs";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "../types";
|
||||||
|
|
||||||
const data: any = { table1: [{ col1: "first col value" }] };
|
const data: any = { table1: [{ col1: "first col value" }] }; // TODO: be more specific on type declaration
|
||||||
|
|
||||||
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||||
name: "SASjs Requests",
|
name: "SASjs Requests",
|
||||||
@@ -9,16 +9,11 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
|||||||
{
|
{
|
||||||
title: "WORK tables",
|
title: "WORK tables",
|
||||||
description: "Should get WORK tables after request",
|
description: "Should get WORK tables after request",
|
||||||
test: async () => {
|
test: async () => adapter.request("common/sendArr", data),
|
||||||
return adapter.request("common/sendArr", data);
|
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
assertion: (res: any) => {
|
||||||
const requests = adapter.getSasRequests();
|
const requests = adapter.getSasRequests();
|
||||||
if (adapter.getSasjsConfig().debug) {
|
|
||||||
return requests[0].SASWORK !== null;
|
return adapter.getSasjsConfig().debug ? requests[0].SASWORK !== null : requests[0].SASWORK === null
|
||||||
} else {
|
|
||||||
return requests[0].SASWORK === null;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import SASjs from "sasjs";
|
import SASjs from "sasjs";
|
||||||
import { TestSuite } from "../types";
|
import { TestSuite } from "../types";
|
||||||
|
|
||||||
const specialCharData: any = {
|
const specialCharData: any = { // TODO: be more specific on type definition
|
||||||
table1: [
|
table1: [
|
||||||
{
|
{
|
||||||
tab: "\t",
|
tab: "\t",
|
||||||
@@ -9,8 +9,8 @@ const specialCharData: any = {
|
|||||||
cr: "\r",
|
cr: "\r",
|
||||||
semicolon: ";semi",
|
semicolon: ";semi",
|
||||||
percent: "%",
|
percent: "%",
|
||||||
singleQuote: "'",
|
singleQuote: "'", // TODO: use ``
|
||||||
doubleQuote: '"',
|
doubleQuote: '"', // TODO: use ``
|
||||||
crlf: "\r\n",
|
crlf: "\r\n",
|
||||||
euro: "€euro",
|
euro: "€euro",
|
||||||
banghash: "!#banghash",
|
banghash: "!#banghash",
|
||||||
@@ -18,7 +18,7 @@ const specialCharData: any = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const moreSpecialCharData: any = {
|
const moreSpecialCharData: any = { // TODO: be more specific on type definition
|
||||||
table1: [
|
table1: [
|
||||||
{
|
{
|
||||||
speech0: '"speech',
|
speech0: '"speech',
|
||||||
@@ -36,44 +36,46 @@ const moreSpecialCharData: any = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWideData = () => {
|
const getWideData = () => { // FIXME: declared but never used
|
||||||
const cols: any = {};
|
const cols: any = {}; // TODO: be more specific on type definition
|
||||||
for (let i = 1; i <= 10000; i++) {
|
for (let i = 1; i <= 10000; i++) { // Why 10000?
|
||||||
cols["col" + i] = "test" + i;
|
cols["col" + i] = "test" + i;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: any = {
|
const data: any = { // TODO: be more specific on type definition
|
||||||
table1: [cols],
|
table1: [cols],
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTables = () => {
|
const getTables = () => { // FIXME: declared but never used
|
||||||
const tables: any = {};
|
const tables: any = {}; // TODO: be more specific on type definition
|
||||||
|
|
||||||
for (let i = 1; i <= 100; i++) {
|
for (let i = 1; i <= 100; i++) { // why 100
|
||||||
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
|
tables["table" + i] = [{ col1: "x", col2: "x", col3: "x", col4: "x" }];
|
||||||
}
|
}
|
||||||
|
|
||||||
return tables;
|
return tables;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLargeDataset = () => {
|
const getLargeDataset = () => {
|
||||||
const rows: any = [];
|
const rows: any = []; // TODO: be more specific on type definition
|
||||||
const colData: string =
|
const colData: string = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // FIXME: no need to explicitly mention data type
|
||||||
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
|
|
||||||
for (let i = 1; i <= 10000; i++) {
|
for (let i = 1; i <= 10000; i++) {
|
||||||
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
|
rows.push({ col1: colData, col2: colData, col3: colData, col4: colData });
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: any = {
|
const data: any = { // TODO: be more specific on type definition
|
||||||
table1: rows,
|
table1: rows,
|
||||||
};
|
};
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorAndCsrfData: any = {
|
// FIXME: declared but never used
|
||||||
|
const errorAndCsrfData: any = { // TODO: be more specific on type definition
|
||||||
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
error: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
||||||
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
_csrf: [{ col1: "q", col2: "w", col3: "e", col4: "r" }],
|
||||||
};
|
};
|
||||||
@@ -84,10 +86,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
{
|
{
|
||||||
title: "Common special characters",
|
title: "Common special characters",
|
||||||
description: "Should handle common special characters",
|
description: "Should handle common special characters",
|
||||||
test: () => {
|
test: () => adapter.request("common/sendArr", specialCharData),
|
||||||
return adapter.request("common/sendArr", specialCharData);
|
assertion: (res: any) => { // TODO: be more specific on type definition
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
|
||||||
return (
|
return (
|
||||||
res.table1[0][0] === specialCharData.table1[0].tab &&
|
res.table1[0][0] === specialCharData.table1[0].tab &&
|
||||||
res.table1[0][1] === specialCharData.table1[0].lf &&
|
res.table1[0][1] === specialCharData.table1[0].lf &&
|
||||||
@@ -102,6 +102,8 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: delete commented out code
|
||||||
|
|
||||||
// {
|
// {
|
||||||
// title: "Other special characters",
|
// title: "Other special characters",
|
||||||
// description: "Should handle other special characters",
|
// description: "Should handle other special characters",
|
||||||
@@ -180,15 +182,15 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
{
|
{
|
||||||
title: "Large dataset",
|
title: "Large dataset",
|
||||||
description: "Should handle 5mb of data",
|
description: "Should handle 5mb of data",
|
||||||
test: () => {
|
test: () => adapter.request("common/sendArr", getLargeDataset()),
|
||||||
return adapter.request("common/sendArr", getLargeDataset());
|
assertion: (res: any) => { // TODO: be more specific on type definition
|
||||||
},
|
|
||||||
assertion: (res: any) => {
|
|
||||||
const data = getLargeDataset();
|
const data = getLargeDataset();
|
||||||
let result = true;
|
let result = true; // TODO: rename
|
||||||
|
|
||||||
for (let i = 0; i <= 10; i++) {
|
for (let i = 0; i <= 10; i++) {
|
||||||
result = result && res.table1[i][0] === data.table1[i][0];
|
result = result && res.table1[i][0] === data.table1[i][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -232,4 +234,4 @@ export const specialCaseTests = (adapter: SASjs): TestSuite => ({
|
|||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -12,4 +12,4 @@ export interface TestSuite {
|
|||||||
tests: Test[];
|
tests: Test[];
|
||||||
beforeAll?: (...args: any) => Promise<any>;
|
beforeAll?: (...args: any) => Promise<any>;
|
||||||
afterAll?: (...args: any) => Promise<any>;
|
afterAll?: (...args: any) => Promise<any>;
|
||||||
}
|
}
|
||||||
@@ -3,20 +3,21 @@ export const assert = (
|
|||||||
message = "Assertion failed"
|
message = "Assertion failed"
|
||||||
) => {
|
) => {
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typeof expression === "boolean") {
|
if (typeof expression === "boolean") result = expression;
|
||||||
result = expression;
|
else result = expression();
|
||||||
} else {
|
|
||||||
result = expression();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!result) {
|
if (!!result) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
console.error(message);
|
console.error(message);
|
||||||
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,21 +3,25 @@ export const uploadFile = (file: File, fileName: string, url: string) => {
|
|||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.append("file", file);
|
data.append("file", file);
|
||||||
data.append("filename", fileName);
|
data.append("filename", fileName);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.withCredentials = true;
|
xhr.withCredentials = true;
|
||||||
xhr.addEventListener("readystatechange", function () {
|
xhr.addEventListener("readystatechange", function () { // TODO: use ES6
|
||||||
if (this.readyState === 4) {
|
if (this.readyState === 4) {
|
||||||
let response: any;
|
let response: any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = JSON.parse(this.responseText);
|
response = JSON.parse(this.responseText);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(response);
|
resolve(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
xhr.setRequestHeader("cache-control", "no-cache");
|
xhr.setRequestHeader("cache-control", "no-cache");
|
||||||
xhr.send(data);
|
xhr.send(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -3,49 +3,49 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export class SAS9ApiClient {
|
export class SAS9ApiClient {
|
||||||
constructor(private serverUrl: string) {}
|
constructor(private serverUrl: string) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* returns on object containing the server URL
|
* @returns an object containing the server URL
|
||||||
*/
|
*/
|
||||||
public getConfig() {
|
public getConfig() {
|
||||||
return {
|
return {
|
||||||
serverUrl: this.serverUrl,
|
serverUrl: this.serverUrl,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates serverurl which is not null
|
* Updates serverUrl which is not null
|
||||||
* @param serverUrl - the URL of the server.
|
* @param serverUrl - the URL of the server.
|
||||||
*/
|
*/
|
||||||
public setConfig(serverUrl: string) {
|
public setConfig(serverUrl: string) {
|
||||||
if (serverUrl) this.serverUrl = serverUrl;
|
if (serverUrl) this.serverUrl = serverUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes code on a SAS9 server.
|
* Executes code on a SAS9 server.
|
||||||
* @param linesOfCode - an array of lines of code to execute
|
* @param linesOfCode - an array of lines of code to execute
|
||||||
* @param serverName - the server to execute the code on
|
* @param serverName - the server to execute the code on
|
||||||
* @param repositoryName - the repository to execute the code on
|
* @param repositoryName - the repository to execute the code on
|
||||||
*/
|
*/
|
||||||
public async executeScript(
|
public async executeScript(
|
||||||
linesOfCode: string[],
|
linesOfCode: string[], // FIXME: rename
|
||||||
serverName: string,
|
serverName: string,
|
||||||
repositoryName: string
|
repositoryName: string
|
||||||
) {
|
) {
|
||||||
const requestPayload = linesOfCode.join("\n");
|
const requestPayload = linesOfCode.join('\n')
|
||||||
const executeScriptRequest = {
|
const executeScriptRequest = {
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
headers: {
|
headers: {Accept: 'application/json'},
|
||||||
Accept: "application/json",
|
body: `command=${requestPayload}`,
|
||||||
},
|
}
|
||||||
body: `command=${requestPayload}`,
|
// FIXME: use axios instead of fetch
|
||||||
};
|
const executeScriptResponse = await fetch(
|
||||||
const executeScriptResponse = await fetch(
|
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
executeScriptRequest
|
||||||
executeScriptRequest
|
).then((res) => res.text())
|
||||||
).then((res) => res.text());
|
// FIXME: no catch block
|
||||||
|
|
||||||
return executeScriptResponse;
|
return executeScriptResponse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* TODO: needs to be split into logical blocks:
|
||||||
|
* - Folder
|
||||||
|
* - Config
|
||||||
|
* - Context
|
||||||
|
* - Session
|
||||||
|
* - Job
|
||||||
|
* - Auth
|
||||||
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isAuthorizeFormRequired,
|
isAuthorizeFormRequired,
|
||||||
parseAndSubmitAuthorizeForm,
|
parseAndSubmitAuthorizeForm,
|
||||||
@@ -32,7 +42,7 @@ export class SASViyaApiClient {
|
|||||||
if (this.rootFolderMap.size) {
|
if (this.rootFolderMap.size) {
|
||||||
return this.rootFolderMap;
|
return this.rootFolderMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.populateRootFolderMap();
|
this.populateRootFolderMap();
|
||||||
return this.rootFolderMap;
|
return this.rootFolderMap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SASjs from "./index";
|
|||||||
|
|
||||||
const adapter = new SASjs();
|
const adapter = new SASjs();
|
||||||
|
|
||||||
|
// FIXME: adapter doesn't have 'parseSAS9SourceCode' and 'parseGeneratedCode'
|
||||||
it("should parse SAS9 source code", async done => {
|
it("should parse SAS9 source code", async done => {
|
||||||
expect(sampleResponse).toBeTruthy();
|
expect(sampleResponse).toBeTruthy();
|
||||||
const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse);
|
const parsedSourceCode = (adapter as any).parseSAS9SourceCode(sampleResponse);
|
||||||
|
|||||||
16
src/SASjs.ts
16
src/SASjs.ts
@@ -1,3 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* TODO: needs to be split into logical blocks:
|
||||||
|
* - Execute
|
||||||
|
* - Context
|
||||||
|
* - Session
|
||||||
|
* - Folder and services
|
||||||
|
* - Job
|
||||||
|
* - Auth
|
||||||
|
* - Config
|
||||||
|
* - Debug
|
||||||
|
* - Response
|
||||||
|
*/
|
||||||
|
|
||||||
import "isomorphic-fetch";
|
import "isomorphic-fetch";
|
||||||
import * as e6p from "es6-promise";
|
import * as e6p from "es6-promise";
|
||||||
(e6p as any).polyfill();
|
(e6p as any).polyfill();
|
||||||
@@ -839,6 +852,7 @@ export default class SASjs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: this method never used
|
||||||
private fetchLogFileContent(logLink: string) {
|
private fetchLogFileContent(logLink: string) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch(logLink, {
|
fetch(logLink, {
|
||||||
@@ -1038,4 +1052,4 @@ export default class SASjs {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
12
src/index.ts
12
src/index.ts
@@ -1,5 +1,7 @@
|
|||||||
import SASjs from "./SASjs";
|
import SASjs from './SASjs'
|
||||||
export * from "./types";
|
|
||||||
export * from "./SASViyaApiClient";
|
export * from './types'
|
||||||
export * from "./SAS9ApiClient";
|
export * from './SASViyaApiClient'
|
||||||
export default SASjs;
|
export * from './SAS9ApiClient'
|
||||||
|
|
||||||
|
export default SASjs
|
||||||
@@ -3,4 +3,4 @@ export interface Context {
|
|||||||
id: string;
|
id: string;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
version: number;
|
version: number;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface CsrfToken {
|
export interface CsrfToken {
|
||||||
headerName: string;
|
headerName: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -4,4 +4,4 @@ export interface Folder {
|
|||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
links: Link[];
|
links: Link[];
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,4 @@ export interface Job {
|
|||||||
createdBy: string;
|
createdBy: string;
|
||||||
links: Link[];
|
links: Link[];
|
||||||
results: JobResult;
|
results: JobResult;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface JobResult {
|
export interface JobResult {
|
||||||
"_webout.json": string;
|
"_webout.json": string;
|
||||||
}
|
}
|
||||||
@@ -4,4 +4,4 @@ export interface Link {
|
|||||||
href: string;
|
href: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
type: string;
|
type: string;
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ export class SASjsConfig {
|
|||||||
* Can be omitted, eg if serving directly from the SAS Web Server or being
|
* Can be omitted, eg if serving directly from the SAS Web Server or being
|
||||||
* streamed.
|
* streamed.
|
||||||
*/
|
*/
|
||||||
|
// TODO: we should clarify what location we are talking about
|
||||||
serverUrl: string = "";
|
serverUrl: string = "";
|
||||||
pathSAS9: string = "";
|
pathSAS9: string = "";
|
||||||
pathSASViya: string = "";
|
pathSASViya: string = "";
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ export interface SASjsRequest {
|
|||||||
generatedCode: string;
|
generatedCode: string;
|
||||||
logFile: string;
|
logFile: string;
|
||||||
SASWORK: any;
|
SASWORK: any;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
* Represents requests that are queued, pending a signon event
|
* Represents requests that are queued, pending a signon event
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// FIXME: be more specific on type declaration
|
||||||
export interface SASjsWaitingRequest {
|
export interface SASjsWaitingRequest {
|
||||||
requestPromise: {
|
requestPromise: {
|
||||||
promise: any;
|
promise: any;
|
||||||
|
|||||||
@@ -5,4 +5,4 @@
|
|||||||
export enum ServerType {
|
export enum ServerType {
|
||||||
SASViya = "SASVIYA",
|
SASViya = "SASVIYA",
|
||||||
SAS9 = "SAS9",
|
SAS9 = "SAS9",
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export interface Session {
|
export interface Session {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
@@ -7,4 +7,4 @@ export * from "./SASjsConfig";
|
|||||||
export * from "./SASjsRequest";
|
export * from "./SASjsRequest";
|
||||||
export * from "./SASjsWaitingRequest";
|
export * from "./SASjsWaitingRequest";
|
||||||
export * from "./ServerType";
|
export * from "./ServerType";
|
||||||
export * from "./Session";
|
export * from "./Session";
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
// FIXME: use ES6
|
||||||
export async function asyncForEach(array: any[], callback: any) {
|
export async function asyncForEach(array: any[], callback: any) {
|
||||||
for (let index = 0; index < array.length; index++) {
|
for (let index = 0; index < array.length; index++) {
|
||||||
await callback(array[index], index, array);
|
await callback(array[index], index, array);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,6 @@ import { SASjsRequest } from "../types/SASjsRequest";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Comparator for SASjs request timestamps
|
* Comparator for SASjs request timestamps
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => {
|
export const compareTimestamps = (a: SASjsRequest, b: SASjsRequest) => {
|
||||||
return b.timestamp.getTime() - a.timestamp.getTime();
|
return b.timestamp.getTime() - a.timestamp.getTime();
|
||||||
|
|||||||
@@ -3,43 +3,43 @@
|
|||||||
* @param data - the JSON object to convert.
|
* @param data - the JSON object to convert.
|
||||||
*/
|
*/
|
||||||
export const convertToCSV = (data: any) => {
|
export const convertToCSV = (data: any) => {
|
||||||
const replacer = (key: any, value: any) => (value === null ? "" : value);
|
const replacer = (key: any, value: any) => (value === null ? "" : value); // FIXME: 'key' parameter was not used, why do we compare with null (undefined, NaN)?
|
||||||
const headerFields = Object.keys(data[0]);
|
const headerFields = Object.keys(data[0]); // FIXME: data can be of any type, but we are working with it as with object
|
||||||
let csvTest;
|
let csvTest;
|
||||||
let invalidString = false;
|
let invalidString = false;
|
||||||
const headers = headerFields.map((field) => {
|
const headers = headerFields.map((field) => {
|
||||||
let firstFoundType: string | null = null;
|
let firstFoundType: string | null = null;
|
||||||
let hasMixedTypes: boolean = false;
|
let hasMixedTypes: boolean = false; // FIXME: unnecessary type declaration
|
||||||
let rowNumError: number = -1;
|
let rowNumError: number = -1; // FIXME: unnecessary type declaration
|
||||||
|
|
||||||
const longestValueForField = data
|
const longestValueForField = data
|
||||||
.map((row: any, index: number) => {
|
.map((row: any, index: number) => { // FIXME: row should be of type string | number
|
||||||
if (row[field] || row[field] === "") {
|
if (row[field] || row[field] === "") {
|
||||||
if (firstFoundType) {
|
if (firstFoundType) {
|
||||||
let currentFieldType =
|
let currentFieldType = // FIXME: use const
|
||||||
row[field] === "" || typeof row[field] === "string"
|
row[field] === "" || typeof row[field] === "string" // FIXME: "" is also of type string
|
||||||
? "chars"
|
? "chars"
|
||||||
: "number";
|
: "number";
|
||||||
|
|
||||||
if (!hasMixedTypes) {
|
if (!hasMixedTypes) {
|
||||||
hasMixedTypes = currentFieldType !== firstFoundType;
|
hasMixedTypes = currentFieldType !== firstFoundType;
|
||||||
rowNumError = hasMixedTypes ? index + 1 : -1;
|
rowNumError = hasMixedTypes ? index + 1 : -1; // TODO: refactor
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (row[field] === "") {
|
if (row[field] === "") {
|
||||||
firstFoundType = "chars";
|
firstFoundType = "chars";
|
||||||
} else {
|
} else {
|
||||||
firstFoundType =
|
firstFoundType =
|
||||||
typeof row[field] === "string" ? "chars" : "number";
|
typeof row[field] === "string" ? "chars" : "number"; // TODO: refactor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let byteSize;
|
let byteSize;
|
||||||
|
|
||||||
if (typeof row[field] === "string") {
|
if (typeof row[field] === "string") {
|
||||||
let doubleQuotesFound = row[field]
|
let doubleQuotesFound = row[field] // FIXME: use const
|
||||||
.split("")
|
.split("")
|
||||||
.filter((char: any) => char === '"');
|
.filter((char: any) => char === '"'); // FIXME: why char is of type any?
|
||||||
|
|
||||||
byteSize = getByteSize(row[field]);
|
byteSize = getByteSize(row[field]);
|
||||||
|
|
||||||
@@ -52,16 +52,18 @@ export const convertToCSV = (data: any) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.sort((a: number, b: number) => b - a)[0];
|
.sort((a: number, b: number) => b - a)[0];
|
||||||
if (longestValueForField && longestValueForField > 32765) {
|
|
||||||
|
if (longestValueForField && longestValueForField > 32765) { // FIXME: longestValueForField is an array and it is not comparable to a number
|
||||||
invalidString = true;
|
invalidString = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasMixedTypes) {
|
if (hasMixedTypes) {
|
||||||
console.error(
|
console.error(
|
||||||
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
`Row (${rowNumError}), Column (${field}) has mixed types: ERROR`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${field}:${firstFoundType === "chars" ? "$" : ""}${
|
return `${field}:${firstFoundType === "chars" ? "$" : ""}${ // TODO: format return string before return statement
|
||||||
longestValueForField
|
longestValueForField
|
||||||
? longestValueForField
|
? longestValueForField
|
||||||
: firstFoundType === "chars"
|
: firstFoundType === "chars"
|
||||||
@@ -73,10 +75,11 @@ export const convertToCSV = (data: any) => {
|
|||||||
if (invalidString) {
|
if (invalidString) {
|
||||||
return "ERROR: LARGE STRING LENGTH";
|
return "ERROR: LARGE STRING LENGTH";
|
||||||
}
|
}
|
||||||
|
|
||||||
csvTest = data.map((row: any) => {
|
csvTest = data.map((row: any) => {
|
||||||
const fields = Object.keys(row).map((fieldName, index) => {
|
const fields = Object.keys(row).map((fieldName, index) => {
|
||||||
let value;
|
let value;
|
||||||
let containsSpecialChar = false;
|
let containsSpecialChar = false; // FIXME: should be const
|
||||||
const currentCell = row[fieldName];
|
const currentCell = row[fieldName];
|
||||||
|
|
||||||
if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) {
|
if (JSON.stringify(currentCell).search(/(\\t|\\n|\\r)/gm) > -1) {
|
||||||
@@ -89,7 +92,7 @@ export const convertToCSV = (data: any) => {
|
|||||||
value = value.replace(/\\\\/gm, "\\");
|
value = value.replace(/\\\\/gm, "\\");
|
||||||
|
|
||||||
if (containsSpecialChar) {
|
if (containsSpecialChar) {
|
||||||
if (value.includes(",") || value.includes('"')) {
|
if (value.includes(",") || value.includes('"')) { // FIXME: use `"`
|
||||||
value = '"' + value + '"';
|
value = '"' + value + '"';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -112,6 +115,7 @@ export const convertToCSV = (data: any) => {
|
|||||||
|
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields.join(",");
|
return fields.join(",");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,6 +125,7 @@ export const convertToCSV = (data: any) => {
|
|||||||
return finalCSV;
|
return finalCSV;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: refactor
|
||||||
const getByteSize = (str: string) => {
|
const getByteSize = (str: string) => {
|
||||||
let byteSize = str.length;
|
let byteSize = str.length;
|
||||||
for (let i = str.length - 1; i >= 0; i--) {
|
for (let i = str.length - 1; i >= 0; i--) {
|
||||||
@@ -130,4 +135,4 @@ const getByteSize = (str: string) => {
|
|||||||
if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
|
if (code >= 0xdc00 && code <= 0xdfff) i--; //trail surrogate
|
||||||
}
|
}
|
||||||
return byteSize;
|
return byteSize;
|
||||||
};
|
};
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export const isAuthorizeFormRequired = (response: string): boolean => {
|
export const isAuthorizeFormRequired = (response: string): boolean => {
|
||||||
return /<form.+action="(.*Logon\/oauth\/authorize[^"]*).*>/gm.test(response);
|
return /<form.+action="(.*Logon\/oauth\/authorize[^"]*).*>/gm.test(response);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export const isLogInRequired = (response: string): boolean => {
|
export const isLogInRequired = (response: string): boolean => {
|
||||||
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm;
|
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/gm; // FIXME: unnecessary type declaration
|
||||||
const matches = pattern.test(response);
|
const matches = pattern.test(response);
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
};
|
};
|
||||||
@@ -1,2 +1 @@
|
|||||||
export const isLogInSuccess = (response: string): boolean =>
|
export const isLogInSuccess = (response: string): boolean => /You have signed in/gm.test(response); // TODO: maybe we have more reliable way to verify that login was successful?
|
||||||
/You have signed in/gm.test(response);
|
|
||||||
@@ -2,30 +2,33 @@ import { CsrfToken } from "../types";
|
|||||||
|
|
||||||
export async function makeRequest<T>(
|
export async function makeRequest<T>(
|
||||||
url: string,
|
url: string,
|
||||||
request: RequestInit,
|
request: RequestInit, // Where 'RequestInit' is coming from?
|
||||||
callback: (value: CsrfToken) => any,
|
callback: (value: CsrfToken) => any,
|
||||||
contentType: "text" | "json" = "json"
|
contentType: "text" | "json" = "json"
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const responseTransform =
|
const responseTransform = contentType === "json" ?
|
||||||
contentType === "json"
|
(res: Response) => res.json()
|
||||||
? (res: Response) => res.json()
|
:
|
||||||
: (res: Response) => res.text();
|
(res: Response) => res.text();
|
||||||
const result = await fetch(url, request).then((response) => {
|
|
||||||
if (!response.ok) {
|
const result = await fetch(url, request).then((response) => { // FIXME: use axios instead of fetch
|
||||||
|
if (!response.ok) { // FIXME we can just check if status === 403
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
const tokenHeader = response.headers.get("X-CSRF-HEADER");
|
||||||
|
|
||||||
if (tokenHeader) {
|
if (tokenHeader) {
|
||||||
const token = response.headers.get(tokenHeader);
|
const token = response.headers.get(tokenHeader) || ''; // TODO: refactor
|
||||||
|
|
||||||
callback({
|
callback({
|
||||||
headerName: tokenHeader,
|
headerName: tokenHeader,
|
||||||
value: token || "",
|
value: token,
|
||||||
});
|
});
|
||||||
|
|
||||||
const retryRequest = {
|
const retryRequest = {
|
||||||
...request,
|
...request,
|
||||||
headers: { ...request.headers, [tokenHeader]: token },
|
headers: { ...request.headers, [tokenHeader]: token },
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch(url, retryRequest).then(responseTransform);
|
return fetch(url, retryRequest).then(responseTransform);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,5 +36,6 @@ export async function makeRequest<T>(
|
|||||||
return responseTransform(response);
|
return responseTransform(response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// TODO: refactor
|
||||||
export const needsRetry = (responseText: string): boolean => {
|
export const needsRetry = (responseText: string): boolean => {
|
||||||
return (
|
return (
|
||||||
(responseText.includes('"errorCode":403') &&
|
(responseText.includes('"errorCode":403') &&
|
||||||
@@ -8,4 +9,4 @@ export const needsRetry = (responseText: string): boolean => {
|
|||||||
(responseText.includes('"status":449') &&
|
(responseText.includes('"status":449') &&
|
||||||
responseText.includes("Authentication success, retry original request"))
|
responseText.includes("Authentication success, retry original request"))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ export const parseAndSubmitAuthorizeForm = async (
|
|||||||
const params: any = {};
|
const params: any = {};
|
||||||
|
|
||||||
const responseBody = response.split("<body>")[1].split("</body>")[0];
|
const responseBody = response.split("<body>")[1].split("</body>")[0];
|
||||||
const bodyElement = document.createElement("div");
|
const bodyElement = document.createElement("div"); // TODO: rename
|
||||||
bodyElement.innerHTML = responseBody;
|
bodyElement.innerHTML = responseBody;
|
||||||
|
|
||||||
const form = bodyElement.querySelector("#application_authorization");
|
const form = bodyElement.querySelector("#application_authorization");
|
||||||
@@ -24,7 +24,7 @@ export const parseAndSubmitAuthorizeForm = async (
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
for (const key in params) {
|
for (const key in params) { // TODO: use forEach
|
||||||
if (params.hasOwnProperty(key)) {
|
if (params.hasOwnProperty(key)) {
|
||||||
formData.append(key, params[key]);
|
formData.append(key, params[key]);
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ export const parseAndSubmitAuthorizeForm = async (
|
|||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (authUrl) {
|
if (authUrl) {
|
||||||
fetch(authUrl, {
|
fetch(authUrl, { // TODO use axios instead of fetch
|
||||||
method: "POST",
|
method: "POST",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: formData,
|
body: formData,
|
||||||
@@ -46,4 +46,4 @@ export const parseAndSubmitAuthorizeForm = async (
|
|||||||
reject("Auth form url is null");
|
reject("Auth form url is null");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
export const parseGeneratedCode = (log: string) => {
|
export const parseGeneratedCode = (log: string) => {
|
||||||
const startsWith = "MPRINT";
|
const startsWith = "MPRINT";
|
||||||
const isGeneratedCodeLine = (line: string) =>
|
const isGeneratedCodeLine = (line: string) => line.trim().startsWith(startsWith);
|
||||||
line.trim().startsWith(startsWith);
|
|
||||||
const logLines = log.split("\n").filter(isGeneratedCodeLine);
|
const logLines = log.split("\n").filter(isGeneratedCodeLine);
|
||||||
|
|
||||||
return logLines.join("\r\n");
|
return logLines.join("\r\n");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
export const parseSasViyaLog = (logResponse: { items: any[] }) => {
|
export const parseSasViyaLog = (logResponse: { items: any[] }) => { // TODO: be more specific on type declaration
|
||||||
let log;
|
let log;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
log = logResponse.items
|
log = logResponse.items
|
||||||
? logResponse.items.map((i) => i.line).join("\n")
|
? logResponse.items.map((i) => i.line).join("\n")
|
||||||
: JSON.stringify(logResponse);
|
: JSON.stringify(logResponse);
|
||||||
} catch (e) {
|
} catch (e) { // TODO: rename parameter to err or error
|
||||||
console.error("An error has occurred while parsing the log response", e);
|
console.error("An error has occurred while parsing the log response", e);
|
||||||
|
|
||||||
log = logResponse;
|
log = logResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const parseSourceCode = (log: string): string => {
|
export const parseSourceCode = (log: string): string => {
|
||||||
const isSourceCodeLine = (line: string) =>
|
const isSourceCodeLine = (line: string) => line.trim().substring(0, 10).trimStart().match(/^\d/);
|
||||||
line.trim().substring(0, 10).trimStart().match(/^\d/);
|
|
||||||
const logLines = log.split("\n").filter(isSourceCodeLine);
|
const logLines = log.split("\n").filter(isSourceCodeLine);
|
||||||
|
|
||||||
return logLines.join("\r\n");
|
return logLines.join("\r\n");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export const serialize = (obj: any) => {
|
export const serialize = (obj: any) => { // TODO: be more specific on type declaration
|
||||||
const str: any[] = [];
|
const str: any[] = [];
|
||||||
for (const p in obj) {
|
|
||||||
|
for (const p in obj) { // FIXME: name variables properly
|
||||||
if (obj.hasOwnProperty(p)) {
|
if (obj.hasOwnProperty(p)) {
|
||||||
if (obj[p] instanceof Array) {
|
if (obj[p] instanceof Array) {
|
||||||
for (let i = 0, n = obj[p].length; i < n; i++) {
|
for (let i = 0, n = obj[p].length; i < n; i++) {
|
||||||
@@ -11,5 +12,6 @@ export const serialize = (obj: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.join("&");
|
return str.join("&");
|
||||||
};
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
export const splitChunks = (content: string) => {
|
export const splitChunks = (content: string) => { // TODO: set return type
|
||||||
const size = 16000;
|
const size = 16000; // why 16000?
|
||||||
|
|
||||||
const numChunks = Math.ceil(content.length / size);
|
const numChunks = Math.ceil(content.length / size);
|
||||||
const chunks = new Array(numChunks);
|
const chunks = new Array(numChunks);
|
||||||
|
|
||||||
for (let i = 0, o = 0; i < numChunks; ++i, o += size) {
|
for (let i = 0, o = 0; i < numChunks; ++i, o += size) { // FIXME: name variables properly
|
||||||
chunks[i] = content.substr(o, size);
|
chunks[i] = content.substr(o, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user