mirror of
https://github.com/gchq/CyberChef
synced 2024-12-28 05:23:10 +00:00
Added more tests, fixed length count bug and IO clearance bug
This commit is contained in:
parent
8c0e23e196
commit
819e4a574c
6 changed files with 277 additions and 22 deletions
|
@ -149,13 +149,20 @@ class StatusBarPanel {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts the stats of a document
|
* Counts the stats of a document
|
||||||
* @param {Text} doc
|
* @param {EditorState} state
|
||||||
*/
|
*/
|
||||||
updateStats(doc) {
|
updateStats(state) {
|
||||||
const length = this.dom.querySelector(".stats-length-value"),
|
const length = this.dom.querySelector(".stats-length-value"),
|
||||||
lines = this.dom.querySelector(".stats-lines-value");
|
lines = this.dom.querySelector(".stats-lines-value");
|
||||||
length.textContent = doc.length;
|
|
||||||
lines.textContent = doc.lines;
|
let docLength = state.doc.length;
|
||||||
|
// CodeMirror always counts line breaks as one character.
|
||||||
|
// We want to show an accurate reading of how many bytes there are.
|
||||||
|
if (state.lineBreak.length !== 1) {
|
||||||
|
docLength += (state.lineBreak.length * state.doc.lines) - state.doc.lines - 1;
|
||||||
|
}
|
||||||
|
length.textContent = docLength;
|
||||||
|
lines.textContent = state.doc.lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -434,7 +441,7 @@ function makePanel(opts) {
|
||||||
sbPanel.updateEOL(view.state);
|
sbPanel.updateEOL(view.state);
|
||||||
sbPanel.updateCharEnc();
|
sbPanel.updateCharEnc();
|
||||||
sbPanel.updateTiming();
|
sbPanel.updateTiming();
|
||||||
sbPanel.updateStats(view.state.doc);
|
sbPanel.updateStats(view.state);
|
||||||
sbPanel.updateSelection(view.state, false);
|
sbPanel.updateSelection(view.state, false);
|
||||||
sbPanel.monitorHTMLOutput();
|
sbPanel.monitorHTMLOutput();
|
||||||
|
|
||||||
|
@ -450,7 +457,7 @@ function makePanel(opts) {
|
||||||
sbPanel.updateSizing(update.view);
|
sbPanel.updateSizing(update.view);
|
||||||
}
|
}
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
sbPanel.updateStats(update.state.doc);
|
sbPanel.updateStats(update.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1222,12 +1222,6 @@ class InputWaiter {
|
||||||
this.setupInputWorker();
|
this.setupInputWorker();
|
||||||
this.manager.worker.setupChefWorker();
|
this.manager.worker.setupChefWorker();
|
||||||
this.addInput(true);
|
this.addInput(true);
|
||||||
|
|
||||||
// Fire the statechange event as the input has been modified,
|
|
||||||
// leaving enough time for workers to be initialised
|
|
||||||
setTimeout(function() {
|
|
||||||
window.dispatchEvent(this.manager.statechange);
|
|
||||||
}.bind(this), 100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -231,9 +231,13 @@ class OutputWaiter {
|
||||||
*/
|
*/
|
||||||
async setOutput(data, force=false) {
|
async setOutput(data, force=false) {
|
||||||
// Don't do anything if the output hasn't changed
|
// Don't do anything if the output hasn't changed
|
||||||
if (!force && data === this.currentOutputCache) return;
|
if (!force && data === this.currentOutputCache) {
|
||||||
this.currentOutputCache = data;
|
this.manager.controls.hideStaleIndicator();
|
||||||
|
this.toggleLoader(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentOutputCache = data;
|
||||||
this.toggleLoader(true);
|
this.toggleLoader(true);
|
||||||
|
|
||||||
// If data is an ArrayBuffer, convert to a string in the correct character encoding
|
// If data is an ArrayBuffer, convert to a string in the correct character encoding
|
||||||
|
|
|
@ -422,23 +422,207 @@ module.exports = {
|
||||||
"Line endings": browser => {
|
"Line endings": browser => {
|
||||||
/* Dropup works */
|
/* Dropup works */
|
||||||
/* Selecting changes view in input */
|
/* Selecting changes view in input */
|
||||||
|
setInput(browser, MULTI_LINE_STRING);
|
||||||
|
|
||||||
|
// Line endings: LF
|
||||||
|
|
||||||
|
// Input
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(3)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(4)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("3");
|
||||||
|
|
||||||
|
// Output
|
||||||
|
bake(browser);
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(3)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3");
|
||||||
|
|
||||||
|
// Input EOL: VT
|
||||||
|
setEOLSeq(browser, "input", "VT");
|
||||||
|
|
||||||
|
// Input
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#input-text .cm-content .cm-specialChar").text.to.equal("␊");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1");
|
||||||
|
|
||||||
|
// Output
|
||||||
|
bake(browser);
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(3)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(4)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("3");
|
||||||
|
|
||||||
|
// Output EOL: VT
|
||||||
|
setEOLSeq(browser, "output", "VT");
|
||||||
|
|
||||||
|
// Input
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#input-text .cm-content .cm-specialChar").text.to.equal("␊");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("1");
|
||||||
|
|
||||||
|
// Output
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(1)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-specialChar");
|
||||||
|
browser.expect.element("#output-text .cm-content .cm-specialChar").text.to.equal("␊");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("301");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("1");
|
||||||
|
|
||||||
/* Adding new line ending changes output correctly */
|
/* Adding new line ending changes output correctly */
|
||||||
/* Other EOL characters are displayed correctly when not being used to end a line */
|
browser.sendKeys("#input-text .cm-content", browser.Keys.RETURN);
|
||||||
/* Changing in output has the correct effect */
|
|
||||||
|
// Input
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(3)");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("302");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2");
|
||||||
|
|
||||||
|
// Output
|
||||||
|
bake(browser);
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(3)");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("302");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2");
|
||||||
|
|
||||||
|
// Input EOL: CRLF
|
||||||
|
setEOLSeq(browser, "input", "CRLF");
|
||||||
|
// Output EOL: CR
|
||||||
|
setEOLSeq(browser, "output", "CR");
|
||||||
|
browser.sendKeys("#input-text .cm-content", browser.Keys.RETURN);
|
||||||
|
|
||||||
|
// Input
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-content .cm-line:nth-of-type(3)")
|
||||||
|
.waitForElementPresent("#input-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(3)");
|
||||||
|
browser.expect.element("#input-text .cm-content .cm-line:nth-of-type(1) .cm-specialChar:nth-of-type(3)").text.to.equal("␋");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-length-value").text.to.equal("304");
|
||||||
|
browser.expect.element("#input-text .cm-status-bar .stats-lines-value").text.to.equal("2");
|
||||||
|
|
||||||
|
// Output
|
||||||
|
bake(browser);
|
||||||
|
browser
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2)")
|
||||||
|
.waitForElementNotPresent("#output-text .cm-content .cm-line:nth-of-type(3)")
|
||||||
|
.waitForElementPresent("#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar");
|
||||||
|
browser.expect.element("#output-text .cm-content .cm-line:nth-of-type(2) .cm-specialChar").text.to.equal("␊");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-length-value").text.to.equal("304");
|
||||||
|
browser.expect.element("#output-text .cm-status-bar .stats-lines-value").text.to.equal("2");
|
||||||
|
|
||||||
/* Line endings appear in the URL */
|
/* Line endings appear in the URL */
|
||||||
|
browser.assert.urlContains("ieol=%0D%0A");
|
||||||
|
browser.assert.urlContains("oeol=%0D");
|
||||||
|
|
||||||
/* Preserved when changing tabs */
|
/* Preserved when changing tabs */
|
||||||
|
browser
|
||||||
|
.click("#btn-new-tab")
|
||||||
|
.waitForElementVisible("#input-tabs li:nth-of-type(2).active-input-tab");
|
||||||
|
browser.expect.element("#input-text .eol-value").text.that.equals("LF");
|
||||||
|
browser.expect.element("#output-text .eol-value").text.that.equals("LF");
|
||||||
|
|
||||||
|
setEOLSeq(browser, "input", "FF");
|
||||||
|
setEOLSeq(browser, "output", "LS");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.click("#input-tabs li:nth-of-type(1)")
|
||||||
|
.waitForElementVisible("#input-tabs li:nth-of-type(1).active-input-tab");
|
||||||
|
browser.expect.element("#input-text .eol-value").text.that.equals("CRLF");
|
||||||
|
browser.expect.element("#output-text .eol-value").text.that.equals("CR");
|
||||||
},
|
},
|
||||||
|
|
||||||
"File inputs": browser => {
|
"File inputs": browser => {
|
||||||
/* By button */
|
clear(browser);
|
||||||
/* By drag and drop */
|
|
||||||
/* Side panel displays correct info */
|
/* Side panel displays correct info */
|
||||||
|
uploadFile(browser, "files/TowelDay.jpeg");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-thumbnail")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-name")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-size")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-type")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-loaded");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("TowelDay.jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("61,379 bytes");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%");
|
||||||
|
|
||||||
/* Side panel can be hidden */
|
/* Side panel can be hidden */
|
||||||
|
browser
|
||||||
|
.click("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-toggle-hidden")
|
||||||
|
.expect.element("#input-text .cm-file-details").to.have.css("width").which.equals("1px");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.click("#input-text .cm-file-details .file-details-toggle-hidden")
|
||||||
|
.waitForElementNotPresent("#input-text .cm-file-details .file-details-toggle-hidden")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.expect.element("#input-text .cm-file-details").to.have.css("width").which.equals("200px");
|
||||||
},
|
},
|
||||||
|
|
||||||
"Folder inputs": browser => {
|
"Folder inputs": browser => {
|
||||||
/* By button */
|
clear(browser);
|
||||||
/* By drag and drop */
|
|
||||||
|
/* Side panel displays correct info */
|
||||||
|
uploadFolder(browser, "files");
|
||||||
|
|
||||||
|
// Tab 1
|
||||||
|
browser
|
||||||
|
.click("#input-tabs li:nth-of-type(1)")
|
||||||
|
.waitForElementVisible("#input-tabs li:nth-of-type(1).active-input-tab");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-thumbnail")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-name")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-size")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-type")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-loaded");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("TowelDay.jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("61,379 bytes");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%");
|
||||||
|
|
||||||
|
// Tab 2
|
||||||
|
browser
|
||||||
|
.click("#input-tabs li:nth-of-type(2)")
|
||||||
|
.waitForElementVisible("#input-tabs li:nth-of-type(2).active-input-tab");
|
||||||
|
|
||||||
|
browser
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-toggle-shown")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-thumbnail")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-name")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-size")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-type")
|
||||||
|
.waitForElementVisible("#input-text .cm-file-details .file-details-loaded");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-name").text.that.equals("Hitchhikers_Guide.jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-size").text.that.equals("36,595 bytes");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-type").text.that.equals("image/jpeg");
|
||||||
|
browser.expect.element("#input-text .cm-file-details .file-details-loaded").text.that.equals("100%");
|
||||||
},
|
},
|
||||||
|
|
||||||
"Loading from URL": browser => {
|
"Loading from URL": browser => {
|
||||||
|
@ -516,12 +700,30 @@ function setChrEnc(browser, io, enc) {
|
||||||
browser
|
browser
|
||||||
.useCss()
|
.useCss()
|
||||||
.click(io + " .chr-enc-value")
|
.click(io + " .chr-enc-value")
|
||||||
.waitForElementVisible(io + " .cm-status-bar-select-scroll")
|
.waitForElementVisible(io + " .chr-enc-select .cm-status-bar-select-scroll")
|
||||||
.click("link text", enc)
|
.click("link text", enc)
|
||||||
.waitForElementNotVisible(io + " .cm-status-bar-select-scroll")
|
.waitForElementNotVisible(io + " .chr-enc-select .cm-status-bar-select-scroll")
|
||||||
.expect.element(io + " .chr-enc-value").text.that.equals(enc);
|
.expect.element(io + " .chr-enc-value").text.that.equals(enc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @function
|
||||||
|
* Sets the end of line sequence in the input or output
|
||||||
|
*
|
||||||
|
* @param {Browser} browser - Nightwatch client
|
||||||
|
* @param {string} io - Either "input" or "output"
|
||||||
|
* @param {string} eol - The sequence to set
|
||||||
|
*/
|
||||||
|
function setEOLSeq(browser, io, eol) {
|
||||||
|
io = `#${io}-text`;
|
||||||
|
browser
|
||||||
|
.useCss()
|
||||||
|
.click(io + " .eol-value")
|
||||||
|
.waitForElementVisible(io + " .eol-select .cm-status-bar-select-content")
|
||||||
|
.click(`${io} .cm-status-bar-select-content a[data-val=${eol}]`)
|
||||||
|
.waitForElementNotVisible(io + " .eol-select .cm-status-bar-select-content")
|
||||||
|
.expect.element(io + " .eol-value").text.that.equals(eol);
|
||||||
|
}
|
||||||
|
|
||||||
/** @function
|
/** @function
|
||||||
* Copies whatever is currently selected
|
* Copies whatever is currently selected
|
||||||
*
|
*
|
||||||
|
@ -609,3 +811,51 @@ function expectOutput(browser, expected) {
|
||||||
return expected === window.app.manager.output.outputEditorView.state.doc.toString();
|
return expected === window.app.manager.output.outputEditorView.state.doc.toString();
|
||||||
}, [expected]);
|
}, [expected]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @function
|
||||||
|
* Uploads a file using the #open-file input
|
||||||
|
*
|
||||||
|
* @param {Browser} browser - Nightwatch client
|
||||||
|
* @param {string} filename - A path to a file in the samples directory
|
||||||
|
*/
|
||||||
|
function uploadFile(browser, filename) {
|
||||||
|
const filepath = require("path").resolve(__dirname + "/../samples/" + filename);
|
||||||
|
|
||||||
|
// The file input cannot be interacted with by nightwatch while it is hidden,
|
||||||
|
// so we temporarily expose it for the purposes of this test.
|
||||||
|
browser.execute(() => {
|
||||||
|
document.getElementById("open-file").style.display = "block";
|
||||||
|
});
|
||||||
|
browser
|
||||||
|
.pause(100)
|
||||||
|
.setValue("#open-file", filepath)
|
||||||
|
.pause(100);
|
||||||
|
browser.execute(() => {
|
||||||
|
document.getElementById("open-file").style.display = "none";
|
||||||
|
});
|
||||||
|
browser.waitForElementVisible("#input-text .cm-file-details");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @function
|
||||||
|
* Uploads a folder using the #open-folder input
|
||||||
|
*
|
||||||
|
* @param {Browser} browser - Nightwatch client
|
||||||
|
* @param {string} foldername - A path to a folder in the samples directory
|
||||||
|
*/
|
||||||
|
function uploadFolder(browser, foldername) {
|
||||||
|
const folderpath = require("path").resolve(__dirname + "/../samples/" + foldername);
|
||||||
|
|
||||||
|
// The folder input cannot be interacted with by nightwatch while it is hidden,
|
||||||
|
// so we temporarily expose it for the purposes of this test.
|
||||||
|
browser.execute(() => {
|
||||||
|
document.getElementById("open-folder").style.display = "block";
|
||||||
|
});
|
||||||
|
browser
|
||||||
|
.pause(100)
|
||||||
|
.setValue("#open-folder", folderpath)
|
||||||
|
.pause(500);
|
||||||
|
browser.execute(() => {
|
||||||
|
document.getElementById("open-folder").style.display = "none";
|
||||||
|
});
|
||||||
|
browser.waitForElementVisible("#input-text .cm-file-details");
|
||||||
|
}
|
||||||
|
|
BIN
tests/samples/files/Hitchhikers_Guide.jpeg
Normal file
BIN
tests/samples/files/Hitchhikers_Guide.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
BIN
tests/samples/files/TowelDay.jpeg
Normal file
BIN
tests/samples/files/TowelDay.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 60 KiB |
Loading…
Reference in a new issue