@curless/hotel-storefront 0.18.3 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/demo-hotels.test.js +21 -2
- package/dist/__tests__/demo-hotels.test.js.map +1 -1
- package/dist/__tests__/server.test.js +17 -3
- package/dist/__tests__/server.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +282 -5
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/backend.d.ts +12 -1
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +7 -1
- package/dist/backend.js.map +1 -1
- package/dist/card.generated.d.ts +1 -1
- package/dist/card.generated.js +2 -2
- package/dist/card.generated.js.map +1 -1
- package/dist/data.d.ts +2 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +24 -7
- package/dist/data.js.map +1 -1
- package/dist/demo-hotels.d.ts +24 -2
- package/dist/demo-hotels.d.ts.map +1 -1
- package/dist/demo-hotels.js +313 -95
- package/dist/demo-hotels.js.map +1 -1
- package/dist/legal.d.ts +1 -1
- package/dist/legal.d.ts.map +1 -1
- package/dist/legal.js +31 -10
- package/dist/legal.js.map +1 -1
- package/dist/limits.d.ts +12 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +47 -0
- package/dist/limits.js.map +1 -0
- package/dist/members.d.ts +9 -0
- package/dist/members.d.ts.map +1 -1
- package/dist/members.js +17 -0
- package/dist/members.js.map +1 -1
- package/dist/money.d.ts +6 -1
- package/dist/money.d.ts.map +1 -1
- package/dist/money.js +6 -1
- package/dist/money.js.map +1 -1
- package/dist/present.d.ts +2 -1
- package/dist/present.d.ts.map +1 -1
- package/dist/present.js +3 -2
- package/dist/present.js.map +1 -1
- package/dist/server.d.ts +8 -8
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -4
- package/dist/server.js.map +1 -1
- package/dist/surface.d.ts +2 -2
- package/dist/surface.d.ts.map +1 -1
- package/dist/surface.js +2 -2
- package/dist/surface.js.map +1 -1
- package/dist/tools.d.ts +39 -13
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +376 -95
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +12 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
2
2
|
import { beforeEach, describe, expect, it } from 'vitest';
|
|
3
3
|
import { loadHotelData } from '../data.js';
|
|
4
4
|
import { createDemoHotels } from '../demo-hotels.js';
|
|
5
|
+
import { MAX_MISSES, Misses } from '../limits.js';
|
|
5
6
|
import { MemberStore } from '../members.js';
|
|
6
7
|
import { handleTool } from '../tools.js';
|
|
7
8
|
import { TripStore } from '../trip.js';
|
|
@@ -195,7 +196,7 @@ describe('building a trip', () => {
|
|
|
195
196
|
expect(both.structured.currencies).toEqual(['HKD', 'CNY']);
|
|
196
197
|
const groups = both.structured.groups;
|
|
197
198
|
expect(groups.map((g) => g.currency)).toEqual(['HKD', 'CNY']);
|
|
198
|
-
expect(both.structured.total).toBe(`${groups[0]?.total}
|
|
199
|
+
expect(both.structured.total).toBe(`${groups[0]?.total} · ${groups[1]?.total}`);
|
|
199
200
|
expect(both.structured.dropped).toBeUndefined();
|
|
200
201
|
expect(both.text).toMatch(/2 currencies, priced separately and not converted/);
|
|
201
202
|
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
@@ -835,7 +836,16 @@ describe('add-ons on a booking already made', () => {
|
|
|
835
836
|
expect(again.structured.forBooking.held).toEqual({
|
|
836
837
|
'airport-transfer': 1,
|
|
837
838
|
});
|
|
838
|
-
// And off again.
|
|
839
|
+
// And off again - once the guest has seen what that does. A confirmation
|
|
840
|
+
// with no preview before it IS the preview (2026-09-28 audit).
|
|
841
|
+
const unseen = await call(t.changeBooking, {
|
|
842
|
+
booking: code,
|
|
843
|
+
extra: 'airport-transfer',
|
|
844
|
+
quantity: 0,
|
|
845
|
+
confirmed: true,
|
|
846
|
+
});
|
|
847
|
+
expect(unseen.structured.view).toBe('change-preview');
|
|
848
|
+
expect(unseen.text).toMatch(/Nothing has changed yet/);
|
|
839
849
|
const off = await call(t.changeBooking, {
|
|
840
850
|
booking: code,
|
|
841
851
|
extra: 'airport-transfer',
|
|
@@ -1024,7 +1034,9 @@ describe('choosing where before which hotel', () => {
|
|
|
1024
1034
|
// Every hotel counted once, down to its city.
|
|
1025
1035
|
const cities = china?.countries.flatMap((c) => c.cities) ?? [];
|
|
1026
1036
|
expect(cities.reduce((n, c) => n + c.count, 0)).toBe(res.structured.total);
|
|
1027
|
-
expect(res.text).toMatch(/
|
|
1037
|
+
expect(res.text).toMatch(/to choose from by region, country and city - /);
|
|
1038
|
+
// Every place, for a host with no card.
|
|
1039
|
+
expect(res.text).toMatch(/China \(5\)/);
|
|
1028
1040
|
expect(res.structured.hotels).toBeUndefined();
|
|
1029
1041
|
});
|
|
1030
1042
|
it('answers a country of many cities with its cities, and `all` with its hotels', async () => {
|
|
@@ -1053,7 +1065,7 @@ describe('choosing where before which hotel', () => {
|
|
|
1053
1065
|
const scope = res.structured.scope;
|
|
1054
1066
|
expect(scope.name).toBe('China');
|
|
1055
1067
|
expect(scope.cities.length).toBeGreaterThanOrEqual(4);
|
|
1056
|
-
expect(res.text).toMatch(/`all` lists
|
|
1068
|
+
expect(res.text).toMatch(/`all` lists every one in China/);
|
|
1057
1069
|
const all = await call(t.listHotels, { city: 'China', all: true });
|
|
1058
1070
|
expect(all.structured.view).toBe('hotels');
|
|
1059
1071
|
});
|
|
@@ -1079,9 +1091,61 @@ describe('finding a booking made elsewhere', () => {
|
|
|
1079
1091
|
expect(byEmail.structured.count).toBe(0);
|
|
1080
1092
|
const both = await handleTool(deps, t.listBookings, { email: 'robin@example.com', booking: code }, elsewhere);
|
|
1081
1093
|
expect(both.structured.count).toBe(1);
|
|
1082
|
-
|
|
1094
|
+
// Found, it is this conversation's now: its code alone opens it here, so
|
|
1095
|
+
// the card's Cancel - which sends nothing more - works on it.
|
|
1096
|
+
const cancel = await handleTool(deps, t.previewCancellation, { booking: code }, elsewhere);
|
|
1097
|
+
expect(cancel.structured.view).toBe('cancellation-preview');
|
|
1098
|
+
// And nothing in any answer carries the conversation it was made in.
|
|
1099
|
+
expect(JSON.stringify(both.structured)).not.toContain('session-');
|
|
1100
|
+
expect(JSON.stringify(both.structured)).not.toContain('owners');
|
|
1101
|
+
const third = { session: 'session-third' };
|
|
1102
|
+
const wrong = await handleTool(deps, t.listBookings, { email: 'someone@example.com', booking: code }, third);
|
|
1083
1103
|
expect(wrong.structured.count).toBe(0);
|
|
1104
|
+
expect(wrong.structured.notFound).toBe(code);
|
|
1084
1105
|
expect(wrong.text).toMatch(/Both are needed/);
|
|
1106
|
+
// The id is not a code: with an email, only the code opens a booking.
|
|
1107
|
+
const id = both.structured.bookings[0]?.id ?? '';
|
|
1108
|
+
const byId = await handleTool(deps, t.listBookings, { email: 'robin@example.com', booking: id }, third);
|
|
1109
|
+
expect(byId.structured.count).toBe(0);
|
|
1110
|
+
});
|
|
1111
|
+
it('says a miss even when the conversation has bookings to list', async () => {
|
|
1112
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1113
|
+
await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1114
|
+
const res = await call(t.listBookings, { booking: 'SL-NOPE00', email: 'robin@example.com' });
|
|
1115
|
+
expect(res.structured.count).toBe(1);
|
|
1116
|
+
expect(res.structured.notFound).toBe('SL-NOPE00');
|
|
1117
|
+
expect(res.text).toMatch(/^No booking matches that confirmation code and email/);
|
|
1118
|
+
});
|
|
1119
|
+
it('stops taking guesses at a code and an email after a few misses', async () => {
|
|
1120
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1121
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1122
|
+
const code = made.structured.booking.confirmationCode;
|
|
1123
|
+
const guessing = { deps: { ...deps, misses: new Misses(() => clock) } };
|
|
1124
|
+
const guess = (booking, session) => handleTool(guessing.deps, t.listBookings, { email: 'robin@example.com', booking }, { session });
|
|
1125
|
+
for (let i = 0; i < MAX_MISSES; i += 1)
|
|
1126
|
+
await guess(`SL-WRONG${i}`, 'session-guesser');
|
|
1127
|
+
// The right code now waits too - from this conversation, and for this
|
|
1128
|
+
// email from another one.
|
|
1129
|
+
const blocked = await guess(code, 'session-guesser');
|
|
1130
|
+
expect(blocked.structured.count).toBe(0);
|
|
1131
|
+
expect(blocked.structured.tooMany).toBe(true);
|
|
1132
|
+
expect(blocked.text).toMatch(/paused for a quarter of an hour/);
|
|
1133
|
+
expect((await guess(code, 'session-other')).structured.tooMany).toBe(true);
|
|
1134
|
+
// A conversation's own booking, with no email, is never a guess.
|
|
1135
|
+
expect((await call(t.listBookings, {})).structured.count).toBe(1);
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
// ⚠️ A CONFIRMATION CONFIRMS WHAT WAS SHOWN (2026-09-28 audit).
|
|
1139
|
+
describe('confirming a cancellation', () => {
|
|
1140
|
+
it('shows the fee first when nothing was previewed, then cancels on the next confirmation', async () => {
|
|
1141
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1142
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1143
|
+
const code = made.structured.booking.confirmationCode;
|
|
1144
|
+
const first = await call(t.cancelBooking, { booking: code, confirmed: true });
|
|
1145
|
+
expect(first.structured.view).toBe('cancellation-preview');
|
|
1146
|
+
expect(first.text).toMatch(/Nothing is cancelled yet/);
|
|
1147
|
+
const second = await call(t.cancelBooking, { booking: code, confirmed: true });
|
|
1148
|
+
expect(second.structured.view).toBe('cancelled');
|
|
1085
1149
|
});
|
|
1086
1150
|
});
|
|
1087
1151
|
// ⚠️ A PLACE, THEN THE DATES, THEN HOTELS PRICED FOR THEM (robin, 2026-09-28,
|
|
@@ -1177,4 +1241,217 @@ describe('a list one filter emptied', () => {
|
|
|
1177
1241
|
expect(res.text).not.toMatch(/together/);
|
|
1178
1242
|
});
|
|
1179
1243
|
});
|
|
1244
|
+
// ⚠️ THE 2026-09-28 FULL AUDIT, server side: each of these was reproduced
|
|
1245
|
+
// against the running connector before it was fixed.
|
|
1246
|
+
describe('the audit of 2026-09-28', () => {
|
|
1247
|
+
const pressed = (c, group) => group === 'brands'
|
|
1248
|
+
? { brand: c.name }
|
|
1249
|
+
: group === 'facilities'
|
|
1250
|
+
? { facilities: [c.key] }
|
|
1251
|
+
: { [String(c.key)]: true };
|
|
1252
|
+
it.each([
|
|
1253
|
+
['a ceiling', { city: 'Hong Kong', maxNightly: 3000 }],
|
|
1254
|
+
['a ceiling on named nights', { city: 'Hong Kong', maxNightly: 3000, ...STAY }],
|
|
1255
|
+
])('counts every filter chip as what pressing it lists, under %s', async (_, args) => {
|
|
1256
|
+
const res = await call(t.listHotels, args);
|
|
1257
|
+
const refine = res.structured.refine;
|
|
1258
|
+
for (const group of ['brands', 'facilities', 'room']) {
|
|
1259
|
+
for (const chip of refine[group]) {
|
|
1260
|
+
if (chip.on)
|
|
1261
|
+
continue;
|
|
1262
|
+
const after = await call(t.listHotels, { ...args, ...pressed(chip, group) });
|
|
1263
|
+
expect([group, chip.name ?? chip.key, after.structured.total ?? 0]).toEqual([
|
|
1264
|
+
group,
|
|
1265
|
+
chip.name ?? chip.key,
|
|
1266
|
+
chip.count,
|
|
1267
|
+
]);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
});
|
|
1271
|
+
it('says a ceiling emptied a filtered list, not the filter', async () => {
|
|
1272
|
+
const res = await call(t.listHotels, { city: 'Hong Kong', brand: 'Kerry', maxNightly: 1000 });
|
|
1273
|
+
expect(res.text).toMatch(/^Nothing under HK\$1,000 a night/);
|
|
1274
|
+
expect(res.text).not.toMatch(/None of the/);
|
|
1275
|
+
});
|
|
1276
|
+
it('keeps the order and the ceiling on the way out of a filter that emptied the list', async () => {
|
|
1277
|
+
const res = await call(t.listHotels, {
|
|
1278
|
+
city: 'Hong Kong',
|
|
1279
|
+
brand: 'Kerry',
|
|
1280
|
+
facilities: ['spa'],
|
|
1281
|
+
sort: 'rating',
|
|
1282
|
+
...STAY,
|
|
1283
|
+
});
|
|
1284
|
+
if (res.structured.ask) {
|
|
1285
|
+
expect(res.structured.ask).toMatchObject({ sort: 'rating', city: 'Hong Kong' });
|
|
1286
|
+
expect(res.structured.stay).toMatchObject(STAY);
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
it('keeps `all` on the search it echoes, so a re-query stays a list', async () => {
|
|
1290
|
+
const res = await call(t.listHotels, { all: true });
|
|
1291
|
+
expect(res.structured.view).toBe('hotels');
|
|
1292
|
+
expect(res.structured.ask).toMatchObject({ all: true });
|
|
1293
|
+
const again = await call(t.listHotels, res.structured.ask);
|
|
1294
|
+
expect(again.structured.view).toBe('hotels');
|
|
1295
|
+
});
|
|
1296
|
+
it('puts hotels with no room for the nights last, and never counts them over a ceiling', async () => {
|
|
1297
|
+
const three = { ...STAY, guests: 3 };
|
|
1298
|
+
const high = await call(t.listHotels, { city: 'Hong Kong', ...three, sort: 'price-high' });
|
|
1299
|
+
const tiles = high.structured.hotels;
|
|
1300
|
+
const firstFull = tiles.findIndex((h) => h.noRoomsForStay);
|
|
1301
|
+
expect(firstFull).toBeGreaterThan(0);
|
|
1302
|
+
expect(tiles.slice(firstFull).every((h) => h.noRoomsForStay)).toBe(true);
|
|
1303
|
+
const capped = await call(t.listHotels, { city: 'Hong Kong', ...three, maxNightly: 100000 });
|
|
1304
|
+
expect(capped.structured.overBudget).toBeUndefined();
|
|
1305
|
+
expect(capped.text).not.toMatch(/over the ceiling/);
|
|
1306
|
+
});
|
|
1307
|
+
it('quotes the misses on named nights at what those nights cost', async () => {
|
|
1308
|
+
const weekend = { checkIn: '2026-10-23', checkOut: '2026-10-25', guests: 2 };
|
|
1309
|
+
const res = await call(t.listHotels, { city: 'Hong Kong', ...weekend, maxNightly: 500 });
|
|
1310
|
+
const [first] = res.structured.nearest;
|
|
1311
|
+
expect(first?.forStay?.nightly).toBeDefined();
|
|
1312
|
+
expect(res.text).toContain(`at ${first?.forStay?.nightly}`);
|
|
1313
|
+
});
|
|
1314
|
+
it('says a widened search came back over the ceiling rather than empty', async () => {
|
|
1315
|
+
const res = await call(t.listHotels, { city: 'Hong Kong', query: 'helipad', maxNightly: 500 });
|
|
1316
|
+
expect(res.text).toMatch(/Nothing matched Hong Kong and "helipad", so this looked in Hong Kong/);
|
|
1317
|
+
expect(res.text).toMatch(/Nothing under HK\$500 a night/);
|
|
1318
|
+
expect(res.text).not.toMatch(/found nothing either/);
|
|
1319
|
+
});
|
|
1320
|
+
it('offers no free cancellation once the window has closed', async () => {
|
|
1321
|
+
// Arriving tomorrow: a 48-hour rule closed yesterday; a 24-hour one has not.
|
|
1322
|
+
const soon = { checkIn: '2026-10-02', checkOut: '2026-10-03', guests: 2 };
|
|
1323
|
+
const island = await call(t.getHotel, { hotel: 'hong-kong-island', ...soon });
|
|
1324
|
+
const rooms = island.structured.rooms;
|
|
1325
|
+
expect(rooms.some((r) => r.refundable)).toBe(true);
|
|
1326
|
+
expect(rooms.every((r) => !r.freeCancellationUntil)).toBe(true);
|
|
1327
|
+
expect(island.text).not.toMatch(/free to cancel/);
|
|
1328
|
+
const free = await call(t.listHotels, { city: 'Hong Kong', ...soon, freeCancellation: true });
|
|
1329
|
+
const ids = free.structured.hotels.map((h) => h.id).sort();
|
|
1330
|
+
expect(ids).toEqual(['hong-kong-jen', 'hong-kong-kowloon']);
|
|
1331
|
+
});
|
|
1332
|
+
it('adds an add-on without re-pricing the room, whoever is signed in now', async () => {
|
|
1333
|
+
await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
|
|
1334
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'horizon-club', ...STAY });
|
|
1335
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1336
|
+
const booking = made.structured.booking;
|
|
1337
|
+
await call(t.signOut);
|
|
1338
|
+
const extras = await call(t.listExtras, { booking: booking.confirmationCode });
|
|
1339
|
+
const transfer = extras.structured.extras.find((e) => e.id === 'airport-transfer');
|
|
1340
|
+
const pre = await call(t.previewChange, {
|
|
1341
|
+
booking: booking.confirmationCode,
|
|
1342
|
+
extra: 'airport-transfer',
|
|
1343
|
+
quantity: 1,
|
|
1344
|
+
});
|
|
1345
|
+
// The difference is the transfer and its tax - nothing from the room.
|
|
1346
|
+
const price = Number(String(transfer?.price).replace(/[^\d.]/g, ''));
|
|
1347
|
+
const diff = Number(String(pre.structured.difference).replace(/[^\d.]/g, ''));
|
|
1348
|
+
expect(diff).toBe(price + Math.round((price * 13) / 100));
|
|
1349
|
+
expect(pre.structured.refundDue).toBe(false);
|
|
1350
|
+
});
|
|
1351
|
+
it('names what a change changes', async () => {
|
|
1352
|
+
await call(t.addToTrip, { hotel: 'hong-kong-kowloon', room: 'deluxe', rooms: 2, ...STAY });
|
|
1353
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1354
|
+
const code = made.structured.booking.confirmationCode;
|
|
1355
|
+
const fewer = await call(t.previewChange, { booking: code, rooms: 1 });
|
|
1356
|
+
expect(fewer.text).toMatch(/would go from 2 rooms to 1 room\./);
|
|
1357
|
+
const none = await call(t.previewChange, { booking: code, rooms: 0 });
|
|
1358
|
+
expect(none.structured.changeable).toBe(false);
|
|
1359
|
+
expect(none.text).toMatch(/at least one room/);
|
|
1360
|
+
const same = await call(t.previewChange, {
|
|
1361
|
+
booking: code,
|
|
1362
|
+
checkOut: '2026-10-22',
|
|
1363
|
+
extra: 'airport-transfer',
|
|
1364
|
+
quantity: 0,
|
|
1365
|
+
});
|
|
1366
|
+
expect(same.text).toMatch(/would move to 2026-10-19 - 2026-10-22\./);
|
|
1367
|
+
});
|
|
1368
|
+
it('charges a non-refundable cancellation exactly what was booked', async () => {
|
|
1369
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'horizon-club', ...STAY });
|
|
1370
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1371
|
+
const booking = made.structured.booking;
|
|
1372
|
+
const pre = await call(t.previewCancellation, { booking: booking.confirmationCode });
|
|
1373
|
+
expect(pre.structured.fee).toBe(booking.quote.total);
|
|
1374
|
+
expect(pre.structured.refund).toBe('HK$0');
|
|
1375
|
+
});
|
|
1376
|
+
it('makes a trip total exactly the sum of the bookings it becomes', async () => {
|
|
1377
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1378
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', extra: 'airport-transfer', quantity: 1 });
|
|
1379
|
+
const later = { checkIn: '2026-10-21', checkOut: '2026-10-23', guests: 2 };
|
|
1380
|
+
await call(t.addToTrip, { hotel: 'hong-kong-kowloon', room: 'deluxe', ...later });
|
|
1381
|
+
await call(t.addToTrip, { hotel: 'hong-kong-kowloon', extra: 'airport-transfer', quantity: 1 });
|
|
1382
|
+
const trip = await call(t.viewTrip);
|
|
1383
|
+
const made = await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1384
|
+
const amount = (x) => Number(x.replace(/[^\d.]/g, ''));
|
|
1385
|
+
const sum = made.structured.bookings.reduce((n, b) => n + amount(b.total), 0);
|
|
1386
|
+
expect(sum).toBe(amount(String(trip.structured.total)));
|
|
1387
|
+
});
|
|
1388
|
+
it('forgets preferences on every conversation signed in as that member', async () => {
|
|
1389
|
+
const other = { session: 'session-other' };
|
|
1390
|
+
const member = { number: 'SLC-4471-8820', surname: 'Ruan' };
|
|
1391
|
+
await call(t.signIn, member);
|
|
1392
|
+
await handleTool(deps, t.signIn, member, other);
|
|
1393
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1394
|
+
await call(t.requestBooking, {
|
|
1395
|
+
name: 'Robin Ruan',
|
|
1396
|
+
email: 'robin@example.com',
|
|
1397
|
+
requests: ['king-bed', 'high-floor'],
|
|
1398
|
+
rememberRequests: true,
|
|
1399
|
+
});
|
|
1400
|
+
await handleTool(deps, t.forgetPreferences, {}, other);
|
|
1401
|
+
const here = await call(t.viewTrip);
|
|
1402
|
+
expect(here.structured.member.preferences).toBeUndefined();
|
|
1403
|
+
});
|
|
1404
|
+
it('adds to saved preferences rather than replacing them, and never keeps an accessible room', async () => {
|
|
1405
|
+
await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
|
|
1406
|
+
const book = async (requests) => {
|
|
1407
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'deluxe-city', ...STAY });
|
|
1408
|
+
return call(t.requestBooking, {
|
|
1409
|
+
name: 'Robin Ruan',
|
|
1410
|
+
email: 'robin@example.com',
|
|
1411
|
+
requests,
|
|
1412
|
+
rememberRequests: true,
|
|
1413
|
+
});
|
|
1414
|
+
};
|
|
1415
|
+
await book(['king-bed', 'high-floor']);
|
|
1416
|
+
const second = await book(['quiet-room', 'accessible-room', 'twin-beds']);
|
|
1417
|
+
expect(second.structured.remembered).toEqual(['high-floor', 'quiet-room', 'twin-beds']);
|
|
1418
|
+
});
|
|
1419
|
+
it('compares at the member rate the list and the hotel show', async () => {
|
|
1420
|
+
await call(t.signIn, { number: 'SLC-4471-8820', surname: 'Ruan' });
|
|
1421
|
+
const hotel = await call(t.getHotel, { hotel: 'hong-kong-kerry', ...STAY });
|
|
1422
|
+
const cheapest = hotel.structured.rooms
|
|
1423
|
+
.map((r) => r.stayTotal)
|
|
1424
|
+
.sort((a, b) => Number(a.replace(/\D/g, '')) - Number(b.replace(/\D/g, '')))[0];
|
|
1425
|
+
const compared = await call(t.compare, {
|
|
1426
|
+
entries: [{ hotel: 'hong-kong-kerry' }, { hotel: 'hong-kong-jen' }],
|
|
1427
|
+
...STAY,
|
|
1428
|
+
});
|
|
1429
|
+
const columns = compared.structured.columns;
|
|
1430
|
+
expect(columns.find((c) => c.hotel.id === 'hong-kong-kerry')?.price?.stayTotal).toBe(cheapest);
|
|
1431
|
+
});
|
|
1432
|
+
it('clamps the room count it says and echoes, and spells a brand as the data does', async () => {
|
|
1433
|
+
const many = await call(t.listHotels, { city: 'Hong Kong', ...STAY, rooms: 9 });
|
|
1434
|
+
expect(many.structured.ask).toMatchObject({ rooms: 5 });
|
|
1435
|
+
expect(many.text).toMatch(/5 rooms/);
|
|
1436
|
+
const kerry = await call(t.listHotels, { city: 'Hong Kong', brand: 'kerry' });
|
|
1437
|
+
const chip = kerry.structured.refine.brands.find((b) => b.name === 'Kerry');
|
|
1438
|
+
expect(chip?.on).toBe(true);
|
|
1439
|
+
});
|
|
1440
|
+
it('says a page past the end is past the end', async () => {
|
|
1441
|
+
const res = await call(t.listHotels, { city: 'Hong Kong', offset: 10 });
|
|
1442
|
+
expect(res.text).toMatch(/past the end: 4 match/);
|
|
1443
|
+
});
|
|
1444
|
+
it('lets a lapsed hold go before pricing a list', async () => {
|
|
1445
|
+
const three = { ...STAY, guests: 3 };
|
|
1446
|
+
await call(t.addToTrip, { hotel: 'hong-kong-island', room: 'suite', ...three });
|
|
1447
|
+
await call(t.requestBooking, { name: 'Robin Ruan', email: 'robin@example.com' });
|
|
1448
|
+
const held = await call(t.listHotels, { city: 'Hong Kong', ...three });
|
|
1449
|
+
const island = () => held.structured.hotels;
|
|
1450
|
+
expect(island().find((h) => h.id === 'hong-kong-island')?.noRoomsForStay).toBeDefined();
|
|
1451
|
+
clock += 3 * 3_600_000;
|
|
1452
|
+
const lapsed = await call(t.listHotels, { city: 'Hong Kong', ...three });
|
|
1453
|
+
const tile = lapsed.structured.hotels.find((h) => h.id === 'hong-kong-island');
|
|
1454
|
+
expect(tile?.noRoomsForStay).toBeUndefined();
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1180
1457
|
//# sourceMappingURL=tools.test.js.map
|