Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/timesync-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ TimeSync.setSyncUrl();

const updateOffset = function () {
const t0 = Date.now();
if (TimeSync.forceDDP || SyncInternals.useDDP) {
if (Meteor.isCordova || TimeSync.forceDDP || SyncInternals.useDDP) {
Meteor.callAsync('_timeSync')
.then((res) => handleResponse(t0, null, res))
.catch((err) => handleResponse(t0, err, null));
Expand Down
131 changes: 131 additions & 0 deletions tests/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,135 @@ describe('Timesync', () => {
}
});
});

// Regression tests for the Cordova/Capacitor fix: on mobile webviews the HTTP
// request to /_timesync can fail (CORS, URL resolution). updateOffset()
// should force DDP transport when Meteor.isCordova is true.
describe('transport selection', () => {
// Spy on the DDP side via Meteor.callAsync monkey-patch, and on the HTTP
// side via PerformanceObserver: `fetch` is imported as a module binding in
// timesync-client.js and cannot be swapped from the test, but every fetch
// still surfaces as a `resource` entry in the Performance API.
function installTransportSpies(syncUrl, counters) {
const originalCallAsync = Meteor.callAsync;
Meteor.callAsync = function (methodName) {
if (methodName === '_timeSync') counters.ddp++;
return originalCallAsync.apply(this, arguments);
};

const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name && entry.name.indexOf(syncUrl) !== -1) counters.http++;
}
});
po.observe({ type: 'resource', buffered: false });

return function restore() {
Meteor.callAsync = originalCallAsync;
po.disconnect();
};
}

it('forces DDP transport when Meteor.isCordova is true', function (done) {
this.timeout(10000);

const originalIsCordova = Meteor.isCordova;
const originalForceDDP = TimeSync.forceDDP;
const originalUseDDP = SyncInternals.useDDP;
const syncUrl = TimeSync.getSyncUrl();

const counters = { ddp: 0, http: 0 };
const restoreSpies = installTransportSpies(syncUrl, counters);

// Simulate a Cordova client whose DDP connection flag has not yet flipped.
// Without the fix, this combination would route through HTTP fetch.
Meteor.isCordova = true;
TimeSync.forceDDP = false;
SyncInternals.useDDP = false;

function cleanup() {
restoreSpies();
Meteor.isCordova = originalIsCordova;
TimeSync.forceDDP = originalForceDDP;
SyncInternals.useDDP = originalUseDDP;
// A second resync rearms the internal setInterval with the restored
// state, avoiding a transport-mismatched timer leaking into later tests.
TimeSync.resync();
}

TimeSync.resync();

simplePoll(
() => counters.ddp >= 1,
() => {
// Give PerformanceObserver a tick to flush any pending resource
// entries before asserting that HTTP was not used.
Meteor.setTimeout(() => {
cleanup();
try {
assert.isAtLeast(counters.ddp, 1,
"Meteor.callAsync('_timeSync') must be used on Cordova");
assert.equal(counters.http, 0,
"HTTP fetch to the sync URL must not be used on Cordova");
done();
} catch (err) {
done(err);
}
}, 100);
},
() => {
cleanup();
done(new Error('Cordova client did not route through DDP within 5s'));
},
5000, 50
);
});

it('uses HTTP transport on a plain browser by default', function (done) {
this.timeout(10000);

const originalIsCordova = Meteor.isCordova;
const originalForceDDP = TimeSync.forceDDP;
const originalUseDDP = SyncInternals.useDDP;
const syncUrl = TimeSync.getSyncUrl();

const counters = { ddp: 0, http: 0 };
const restoreSpies = installTransportSpies(syncUrl, counters);

Meteor.isCordova = false;
TimeSync.forceDDP = false;
SyncInternals.useDDP = false;

function cleanup() {
restoreSpies();
Meteor.isCordova = originalIsCordova;
TimeSync.forceDDP = originalForceDDP;
SyncInternals.useDDP = originalUseDDP;
TimeSync.resync();
}

TimeSync.resync();

simplePoll(
() => counters.http >= 1,
() => {
cleanup();
try {
assert.isAtLeast(counters.http, 1,
'HTTP fetch to the sync URL must be used on a plain browser');
assert.equal(counters.ddp, 0,
"Meteor.callAsync('_timeSync') must not be used on a plain browser");
done();
} catch (err) {
done(err);
}
},
() => {
cleanup();
done(new Error('Browser client did not route through HTTP within 5s'));
},
5000, 50
);
});
});
});
Loading