Skip to content

Commit

Permalink
Add download button (Mr0grog#78)
Browse files Browse the repository at this point in the history
There is now a “download” button alongside the “copy” button for the converted Markdown code. It saves the results to a file named `Converted Text.md`.

This also involved some complex additions to the tests in order to create temp directories that the browsers could download files to and be validated. Running tests now creates directories named `temp/chrome/`, `temp/firefox/`, and `temp/safari/` for each browser to work in. Unfortunately, there doesn’t seem to be an obvious way to set the download path for Safaridriver, so the test for this feature is skipped in that browser. :(

Co-authored-by: Rob Brackett <[email protected]>
  • Loading branch information
jkingsman and Mr0grog authored Jul 28, 2023
1 parent a01cbb3 commit 7df0eae
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 10 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.DS_Store

node_modules
dist
logs
temp
coverage

scratch.*
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ This project is open source, and gets better with the hard work and collaboratio
| Contributions | Name |
| ----: | :---- |
| [💻](# "Code") | [Michael Bianco](https://github.com/iloveitaly) |
| [🚧](# "Maintenance") [💻](# "Code") [🚇](# "Infrastructure") [📖](# "Documentation") [👀](# "Reviewer") | [Rob Brackett](https://github.com/Mr0grog) |
| [💻](# "Code") | [Peter Law](https://github.com/PeterJCLaw) |
| [🚧](# "Maintenance") [💻](# "Code") [🚇](# "Infrastructure") [⚠️](# "Tests") [📖](# "Documentation") [👀](# "Reviewer") | [Rob Brackett](https://github.com/Mr0grog) |
| [💻](# "Code") [⚠️](# "Tests") | [Jack Kingsman](https://github.com/jkingsman) |
| [💻](# "Code") | [Peter Law](https://github.com/PeterJCLaw) |
| [📖](# "Documentation") [🚇](# "Infrastructure") | [Marcin Rataj](https://github.com/lidel) |
| [💻](# "Code") | [Ben Sheldon](https://github.com/bensheldon) |
| [💻](# "Code") | [Ben Sheldon](https://github.com/bensheldon) |
<!-- ALL-CONTRIBUTORS-LIST:END -->

(For a key to the contribution emoji or more info on this format, check out [“All Contributors.”](https://allcontributors.org/docs/en/emoji-key))
Expand Down
7 changes: 5 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
font-style: italic;
}

#copy-button {
#button-container {
position: absolute;
right: 0;
top: 0;
Expand All @@ -118,7 +118,10 @@ <h1>Convert Google Docs to Markdown</h1>
</div>

<div id="output-area">
<button id="copy-button" style="display: none;">Copy Markdown</button>
<div id="button-container">
<button id="download-button" style="display: none;">Download Markdown</button>
<button id="copy-button" style="display: none;">Copy Markdown</button>
</div>
<p class="instructions">…and get your Markdown here</p>
<textarea id="output" class="input-field" autocomplete="off"></textarea>
</div>
Expand Down
27 changes: 27 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,30 @@ if (navigator.clipboard && navigator.clipboard.writeText) {
});
});
}

const downloadButton = document.getElementById('download-button');
if (window.URL && window.File) {
downloadButton.style.display = '';
downloadButton.addEventListener('click', () => {
const file = new File([outputElement.value], 'Converted Text.md', {
type: 'text/markdown',
});

// Make a link to the file and click it to trigger a download. Chrome has a
// fancy API for opening a save dialog, but other browsers do not, and this
// is the most universal way to download a file created in the front-end.
let url, link;
try {
url = URL.createObjectURL(file);
link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
}
finally {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
});
}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"name": "Michael Bianco",
"url": "https://github.com/iloveitaly"
},
{
"name": "Jack Kingsman",
"url": "https://github.com/jkingsman"
},
{
"name": "Peter Law",
"url": "https://github.com/PeterJCLaw"
Expand All @@ -26,7 +30,7 @@
"type": "module",
"scripts": {
"build": "mkdir -p dist && webpack",
"clean": "rm -rf dist/*; rm -rf logs/*",
"clean": "rm -rf dist/*; rm -rf logs/*; rm -rf temp/*",
"start": "mkdir -p dist && webpack serve",
"test": "npm run test-unit && npm run test-e2e",
"test-unit": "wdio run ./wdio.conf.unit.js --coverage",
Expand Down
34 changes: 34 additions & 0 deletions test/e2e/basic.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { browser, $, expect } from '@wdio/globals';
import { getTestTempDirectory, waitForFileExists } from '../support/utils.js';

describe('Basic functionality', () => {
it('should convert input and display in output area', async () => {
Expand Down Expand Up @@ -41,5 +44,36 @@ describe('Basic functionality', () => {
await expect($outputInstructions).not.toBeDisplayed();
});

it('downloads the markdown when the button is clicked', async function() {
if (browser.capabilities.browserName === 'Safari') {
this.skip(
"Test not supported in Safari - we can't choose the download directory."
);
return;
}

await browser.url('/');

const $input = await $('#input');

await $input.click();
// Ideally, this would be `browser.keys([Key.Ctrl, 'b'])`, but only some
// browsers automatically map basic formatting commands to the keyboard.
await browser.execute(() => {
document.execCommand('bold', false, null);
});
await browser.keys('convert me');

const $download_button = await $('#download-button');
await $download_button.click();

const downloadDirectory = getTestTempDirectory(browser);
const filePath = path.join(downloadDirectory, 'Converted Text.md');
await waitForFileExists(filePath);

const fileContents = await fs.readFile(filePath, 'utf-8');
await expect(fileContents).toBe('**convert me**\n');
});

// TODO: test copy button (requires serving over HTTPS in some browsers)
});
65 changes: 65 additions & 0 deletions test/support/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { AssertionError } from 'node:assert';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';

/**
* Get the absolute path a temporary directory that tests can use for working
* with files and that test browsers are configured to download files to.
* @param {string|{browserName: string}|{capabilities: {browserName: string}}} browser
* The name of the browser/test environment to get a temp directory for,
* or a Webdriver capabilities or browser object identifying the browser.
* @returns {string}
*/
export function getTestTempDirectory(browser) {
const browserName = browser?.capabilities?.browserName
?? browser?.browserName
?? browser;
if (typeof browserName !== 'string') {
throw new TypeError('The first argument must be a string or browser/capability object');
}
return path.join(global.tempDirectory, browserName.toLowerCase());
}

/**
* Wait for a file to exist, or reject with an error after timing out.
* @param {string} filePath
* @param {number} [timeout]
* @returns {Promise<void>}
*/
export async function waitForFileExists(filePath, timeout = 5_000) {
const parentPath = path.dirname(filePath);
const basename = path.basename(filePath);

// Start watching first to eliminate any race conditions.
const aborter = new AbortController();
const watcher = fs.watch(parentPath, { signal: aborter.signal });
const timer = setTimeout(() => {
aborter.abort(new AssertionError({
message: `File did not exist at ${filePath} after ${timeout} ms`
}));
}, timeout);

// Check whether the file already exists and stop watching if so.
try {
await fs.access(filePath, fs.constants.F_OK);
aborter.abort('File already exists');
return;
}
catch (_error) {
try {
for await (const { eventType, filename } of watcher) {
if (eventType === 'rename' && filename === basename) {
return;
}
}
}
catch (error) {
// The AbortError you get from watch() is uninformative, so unwrap its
// cause (if present) and throw that instead.
throw error.cause || error;
}
}
finally {
clearTimeout(timer);
}
}
58 changes: 54 additions & 4 deletions wdio.conf.base.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,53 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { getTestTempDirectory } from './test/support/utils.js';

const IS_CI = /^(true|1)$/i.test(process.env.ci?.trim() || '');

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
global.tempDirectory = path.join(__dirname, 'temp');

// Configuration for all testable browsers.
let capabilities = [
{
browserName: 'chrome',
acceptInsecureCerts: true,
'browserName': 'chrome',
'acceptInsecureCerts': true,
'goog:chromeOptions': {
prefs: {
'directory_upgrade': true,
'prompt_for_download': false,
'download.default_directory': getTestTempDirectory('chrome')
},
}
},
{
browserName: 'firefox',
acceptInsecureCerts: true,
'browserName': 'firefox',
'acceptInsecureCerts': true,
'moz:firefoxOptions': {
// For detailed descriptions of Firefox prefs, see:
// - http://kb.mozillazine.org/About:config_entries
// - https://searchfox.org/mozilla-central/source/modules/libpref/init/all.js
prefs: {
// Don't show a dialog.
'browser.download.useDownloadDir': true,
// Use the dir set below.
'browser.download.folderList': 2,
// Only `browser.download.dir` is really required, but set all the
// relevant download directory variables just in case.
'browser.download.dir': getTestTempDirectory('firefox'),
'browser.download.downloadDir': getTestTempDirectory('firefox'),
'browser.download.defaultFolder': getTestTempDirectory('firefox'),
'browser.download.lastDir': getTestTempDirectory('firefox'),
}
}
},
{
browserName: 'safari',
acceptInsecureCerts: true,
// There's no clear way to set the download dir in safaridriver. :(
// Any download-related tests should probably be skipped on Safari.
}
];

Expand Down Expand Up @@ -71,4 +107,18 @@ export const config = {
ui: 'bdd',
timeout: 60_000
},

async onPrepare (_config, capabilities) {
// Ensure we have a clean temp directory.
await fs.rm(global.tempDirectory, { recursive: true, force: true });
await fs.mkdir(global.tempDirectory);

// And a subdirectory for each test environment -- they run in parallel,
// so need their own isolated space to play in.
for (const capability of capabilities) {
const browserDirectory = getTestTempDirectory(capability);
await fs.rm(browserDirectory, { recursive: true, force: true });
await fs.mkdir(browserDirectory);
}
}
};

0 comments on commit 7df0eae

Please sign in to comment.