Skip to content

Commit

Permalink
feat: add possibility to handle access geolocation dialog(closes #3224)…
Browse files Browse the repository at this point in the history
… (#7791)

## Purpose
#3224

In some cases, you need to handle access geolocation dialog and provide
a corresponding result.

## Approach
The geolocation window can be invoked when the user call
[window.navigator.geolocation.getCurrentPosition](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/getCurrentPosition)
or watchPosition.
We cannot emulate watchPosition method because it invokes callbacks on
changing user geolocation. So, this PR only handles getCurrentPosition
method. When this method is called on the page for the first time, the
Geolocation access dialog must be shown. All subsequent calls will lead
to returning geolocation in the case when the user allowed access, or an
error in the case when the user denied access.
This handler allows you to return a geolocation or an error when calling
getCurrentPosition. If an instance of an Error object was returned, the
failCallback function will be called with that error. If any other value
is returned, then successCallback will be called with that value.
Since this dialog is only shown the first time getCurrentPosition is
called, the history contains only one record per URL.


## API
```js

// Test
test('Expected geolocation object and geolocation error returned after an action', async t => {
  await t
    .setNativeDialogHandler((type) => {
        if (type === 'geolocation')
            return { timestamp: 12356, coords: {} };
    
        return null;
    });
    .click('#buttonGeo')
    .setNativeDialogHandler((type) => {
        if (type !== 'geolocation')
            return null;
    
        const err = new Error('Some error');
    
        err.code = 1;
    
        return err;
    })
    .click('#buttonGeo');
    
  const history = await t.getNativeDialogHistory();
  
  // NOTE: Only one record will be added to the history, since dialog appears only on the first geolocation request
  await t.expect(history).eql([{ type: 'geolocation', url: pageUrl }]);
});

// Aplication code:
function successCallback(geolocation) {
    console.log(geolocation) // { timestamp: 12356, coords: {} }
}
function failCallback(err) {
    console.log(err.message); //  "Some error"
    console.log(err.code); // 1
}

function onGeoButtonClick () {
    window.navigator.geolocation.getCurrentPosition(successCallback, failCallback)
}
```

## References
#3224 

## Pre-Merge TODO
- [x] Write tests for your proposed changes
- [x] Make sure that existing tests do not fail
  • Loading branch information
Artem-Babich authored Jul 12, 2023
1 parent bf58d0c commit faed539
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 43 deletions.
59 changes: 46 additions & 13 deletions src/client/driver/native-dialog-tracker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const UNEXPECTED_DIALOG = 'testcafe|native-dialog-tracker|unexpe
const ERROR_IN_HANDLER = 'testcafe|native-dialog-tracker|error-in-handler';
const GETTING_PAGE_URL_PROCESSED_SCRIPT = processScript('window.location.href');
const NATIVE_DIALOG_TYPES = ['alert', 'confirm', 'prompt', 'print'];

const GEOLOCATION_DIALOG_TYPE = 'geolocation';

export default class NativeDialogTracker {
constructor (contextStorage, { dialogHandler } = {}) {
Expand All @@ -22,9 +22,6 @@ export default class NativeDialogTracker {

this._init();
this._initListening();

if (this.dialogHandler)
this.setHandler(dialogHandler);
}

get appearedDialogs () {
Expand Down Expand Up @@ -95,13 +92,13 @@ export default class NativeDialogTracker {
this.contextStorage.save();
});

window.alert = () => this._defaultDialogHandler('alert');
window.confirm = () => this._defaultDialogHandler('confirm');
window.prompt = () => this._defaultDialogHandler('prompt');
window.print = () => this._defaultDialogHandler('print');
this._setCustomOrDefaultHandler();
}

_createDialogHandler (type) {
if (type === GEOLOCATION_DIALOG_TYPE)
return this._createGeolocationHandler();

return text => {
const url = NativeDialogTracker._getPageUrl();

Expand All @@ -121,6 +118,32 @@ export default class NativeDialogTracker {
};
}

_createGeolocationHandler () {
return (successCallback, failCallback) => {
const url = NativeDialogTracker._getPageUrl();
const isFirstGeolocationRequest = !nativeMethods.arraySome
.call(this.appearedDialogs, dialog => dialog.type === GEOLOCATION_DIALOG_TYPE && dialog.url === url);

if (isFirstGeolocationRequest)
this._addAppearedDialogs(GEOLOCATION_DIALOG_TYPE, void 0, url);

const executor = new ClientFunctionExecutor(this.dialogHandler);
let result = null;

try {
result = executor.fn.apply(window, [GEOLOCATION_DIALOG_TYPE, void 0, url]);
}
catch (err) {
this._onHandlerError(GEOLOCATION_DIALOG_TYPE, err.message || String(err), url);
}

if (result instanceof Error)
failCallback(result);
else
successCallback(result);
};
}

// Overridable methods
_defaultDialogHandler (type) {
const url = NativeDialogTracker._getPageUrl();
Expand All @@ -136,15 +159,25 @@ export default class NativeDialogTracker {
this.handlerError = this.handlerError || { type, message, url };
}

_setCustomOrDefaultHandler () {
const geolocation = window.navigator.geolocation;
const createDialogCtor = this.dialogHandler
? dialogType => this._createDialogHandler(dialogType)
: dialogType => () => this._defaultDialogHandler(dialogType);

NATIVE_DIALOG_TYPES.forEach(dialogType => {
window[dialogType] = createDialogCtor(dialogType);
});

if (geolocation?.getCurrentPosition)
geolocation.getCurrentPosition = createDialogCtor(GEOLOCATION_DIALOG_TYPE);
}

// API
setHandler (dialogHandler) {
this.dialogHandler = dialogHandler;

NATIVE_DIALOG_TYPES.forEach(dialogType => {
window[dialogType] = this.dialogHandler ?
this._createDialogHandler(dialogType) :
() => this._defaultDialogHandler(dialogType);
});
this._setCustomOrDefaultHandler();
}

getUnexpectedDialogError () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<button id="buttonAlert">Alert</button>
<button id="buttonConfirm">Confirm</button>
<button id="buttonPrint">Print</button>
<button id="buttonGeo">Geolocation</button>

<button id="buttonDialogAfterTimeoutWithRedirect">DialogAfterTimeoutWithRedirect</button>
<button id="buttonDialogAfterTimeout">DialogAfterTimeout</button>
Expand All @@ -26,6 +27,16 @@
window.alert('Alert!');
});

document.getElementById('buttonGeo').addEventListener('click', function () {
window.navigator.geolocation.getCurrentPosition(
(geo) => document.getElementById('result').textContent = JSON.stringify(geo),
(err) => document.getElementById('result').textContent = JSON.stringify({
message: err.message,
code: err.code
}),
)
});

document.getElementById('buttonConfirm').addEventListener('click', function () {
document.getElementById('result').textContent = window.confirm('Confirm?');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,23 @@
<div style="background-color: #6ab779; width: 100px; height: 100px;;"></div>
<script>
const currentDialogItemName = 'currentDialog';
const promptResultItem = 'promptResult';
const confirmResultItem = 'confirmResult';
const dialogs = {
alert: 'alert',
confirm: 'confirm',
prompt: 'prompt',
print: 'print',
}
const dialogsOrder = [
const promptResultItem = 'promptResult';
const confirmResultItem = 'confirmResult';
const geolocationResultItem = 'geoResult';

const dialogs = {
alert: 'alert',
confirm: 'confirm',
prompt: 'prompt',
print: 'print',
geolocation: 'geolocation',
};
const dialogsOrder = [
dialogs.alert,
dialogs.confirm,
dialogs.prompt,
dialogs.print,
dialogs.geolocation,
];
const currentDialog = sessionStorage.getItem(currentDialogItemName) || dialogsOrder[0];

Expand All @@ -31,10 +35,11 @@

window.getDialogsResult = () => {
return {
prompt: sessionStorage.getItem(promptResultItem),
confirm: sessionStorage.getItem(confirmResultItem),
}
}
prompt: sessionStorage.getItem(promptResultItem),
confirm: sessionStorage.getItem(confirmResultItem),
geolocation: sessionStorage.getItem(geolocationResultItem),
};
};

sessionStorage.setItem(currentDialogItemName, getNextDialog(currentDialog));

Expand All @@ -52,6 +57,10 @@
}
else if (currentDialog === dialogs.print) {
window.print();
document.location.reload();
}
else if (currentDialog === dialogs.geolocation) {
window.navigator.geolocation.getCurrentPosition(geo => sessionStorage.setItem(geolocationResultItem, JSON.stringify(geo)));
sessionStorage.removeItem(currentDialogItemName);
}
</script>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ const pageUrl = 'http://localhost:3000/fixtures/api/es-next/native-dialog
const pageLoadingUrl = 'http://localhost:3000/fixtures/api/es-next/native-dialogs-handling/pages/page-load.html';
const pagePromptUrl = 'http://localhost:3000/fixtures/api/es-next/native-dialogs-handling/pages/prompt.html';


describe('Native dialogs handling', function () {
it('Should remove dialog handler if `null` specified', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Null handler', { shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, getNativeDialogNotHandledErrorText('alert', pageUrl), 0);
errorInEachBrowserContains(errs, '> 216 | .click(\'#buttonAlert\');', 0);
errorInEachBrowserContains(errs, '> 243 | .click(\'#buttonAlert\');', 0);
});
});

Expand All @@ -24,14 +23,18 @@ describe('Native dialogs handling', function () {
{ shouldFail: true, skipJsErrors: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, getNativeDialogNotHandledErrorText('confirm', pageUrl), 0);
errorInEachBrowserContains(errs, '> 17 | await t.click(\'#buttonConfirm\'); ', 0);
errorInEachBrowserContains(errs, '> 15 | await t.click(\'#buttonConfirm\'); ', 0);
});
});

it('Should pass if the expected confirm dialog appears after an action', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Expected confirm after an action');
});

it('Should pass if the expected geolocation dialog appears after an action', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Expected geolocation object and geolocation error returned after an action');
});

it('Should pass if the expected confirm dialog appears after an action (with dependencies)', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Expected confirm after an action (with dependencies)');
});
Expand All @@ -48,7 +51,7 @@ describe('Native dialogs handling', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Confirm dialog with wrong text', { shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, getUncaughtErrorInNativeDialogHandlerText('confirm', 'Wrong dialog text', pageUrl), 0);
errorInEachBrowserContains(errs, '> 106 | .click(\'#buttonConfirm\');', 0);
errorInEachBrowserContains(errs, '> 133 | .click(\'#buttonConfirm\');', 0);
});
});

Expand All @@ -57,7 +60,7 @@ describe('Native dialogs handling', function () {
{ shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, 'AssertionError: expected 0 to deeply equal 1', 0);
errorInEachBrowserContains(errs, ' > 118 | await t.expect(info.length).eql(1);', 0);
errorInEachBrowserContains(errs, ' > 145 | await t.expect(info.length).eql(1);', 0);
});
});

Expand All @@ -73,7 +76,7 @@ describe('Native dialogs handling', function () {
{ shouldFail: true, skipJsErrors: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, getNativeDialogNotHandledErrorText('print', pageUrl), 0);
errorInEachBrowserContains(errs, '> 26 | await t.click(\'#buttonPrint\'); ', 0);
errorInEachBrowserContains(errs, '> 23 | await t.click(\'#buttonPrint\'); ', 0);
});
});

Expand All @@ -93,7 +96,7 @@ describe('Native dialogs handling', function () {
{ shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, getNativeDialogNotHandledErrorText('alert', pageLoadingUrl), 0);
errorInEachBrowserContains(errs, '> 56 | await t.click(\'body\');', 0);
errorInEachBrowserContains(errs, '> 64 | await t.click(\'body\');', 0);
});
});
});
Expand Down Expand Up @@ -124,7 +127,7 @@ describe('Native dialogs handling', function () {
{ shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, 'AssertionError: expected 0 to deeply equal 1', 0);
errorInEachBrowserContains(errs, '> 186 | await t.expect(info.length).eql(1);', 0);
errorInEachBrowserContains(errs, '> 213 | await t.expect(info.length).eql(1);', 0);
});
});

Expand All @@ -142,23 +145,23 @@ describe('Native dialogs handling', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Dialog handler has wrong type', { shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, 'The native dialog handler is expected to be a function, ClientFunction or null, but it was number.', 0);
errorInEachBrowserContains(errs, ' > 197 | await t.setNativeDialogHandler(42);', 0);
errorInEachBrowserContains(errs, ' > 224 | await t.setNativeDialogHandler(42);', 0);
});
});

it('Should fail if client function argument has wrong type', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Client function argument wrong type', { shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, 'Cannot initialize a ClientFunction because ClientFunction is number, and not a function.', 0);
errorInEachBrowserContains(errs, ' > 201 | await t.setNativeDialogHandler(ClientFunction(42));', 0);
errorInEachBrowserContains(errs, ' > 228 | await t.setNativeDialogHandler(ClientFunction(42));', 0);
});
});

it('Should fail if Selector send as dialog handler', function () {
return runTests('./testcafe-fixtures/native-dialogs-test.js', 'Selector as dialogHandler', { shouldFail: true })
.catch(function (errs) {
errorInEachBrowserContains(errs, 'The native dialog handler is expected to be a function, ClientFunction or null, but it was Selector.', 0);
errorInEachBrowserContains(errs, '> 207 | await t.setNativeDialogHandler(dialogHandler);', 0);
errorInEachBrowserContains(errs, '> 234 | await t.setNativeDialogHandler(dialogHandler);', 0);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,10 @@ import { ClientFunction, Selector } from 'testcafe';
fixture `Native dialogs`
.page `http://localhost:3000/fixtures/api/es-next/native-dialogs-handling/pages/index.html`;


const getResult = ClientFunction(() => document.getElementById('result').textContent);
const pageUrl = 'http://localhost:3000/fixtures/api/es-next/native-dialogs-handling/pages/index.html';
const promptPageUrl = 'http://localhost:3000/fixtures/api/es-next/native-dialogs-handling/pages/prompt.html';


test('Without handler', async t => {
const info = await t.getNativeDialogHistory();

Expand All @@ -17,7 +15,6 @@ test('Without handler', async t => {
await t.click('#buttonConfirm');
});


test('Print without handler', async t => {
const info = await t.getNativeDialogHistory();

Expand All @@ -36,6 +33,36 @@ test('Expected print after an action', async t => {
await t.expect(dialogs).eql([{ type: 'print', url: pageUrl }]);
});


test('Expected geolocation object and geolocation error returned after an action', async t => {
await t
.setNativeDialogHandler((type) => {
if (type === 'geolocation')
return { timestamp: 12356, coords: {} };

return null;
})
.click('#buttonGeo')
.expect(getResult()).eql('{"timestamp":12356,"coords":{}}')
.setNativeDialogHandler((type) => {
if (type !== 'geolocation')
return null;

const err = new Error('Some error');

err.code = 1;

return err;
})
.click('#buttonGeo')
.expect(getResult()).eql('{"message":"Some error","code":1}');

const history = await t.getNativeDialogHistory();

// NOTE: Only one record must be added to the history, since dialog appears only on the first geolocation request
await t.expect(history).eql([{ type: 'geolocation', url: pageUrl }]);
});

test('Expected confirm after an action', async t => {
await t
.setNativeDialogHandler((type, text) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ClientFunction } from 'testcafe';

fixture `Page load`;
fixture`Page load`;


const getResult = ClientFunction(() => window.getDialogsResult());
Expand All @@ -16,18 +16,26 @@ test('Expected dialogs after page load', async t => {
if (type === 'prompt')
return 'PromptMsg';

if (type === 'geolocation')
return { geo: 'location' };

return null;
})
.navigateTo(pageUrl);

await t.expect(await getResult()).eql({
prompt: 'PromptMsg',
confirm: 'true',
prompt: 'PromptMsg',
confirm: 'true',
geolocation: '{"geo":"location"}',
});

const info = await t.getNativeDialogHistory();

await t.expect(info).eql([
{
type: 'geolocation',
url: pageUrl,
},
{
type: 'print',
url: pageUrl,
Expand Down

0 comments on commit faed539

Please sign in to comment.