A Caching Method in PWA - progressive-web-apps

const cacheName = "the-cache-version-1";
self.addEventListener('install', (e) => {
console.log('[Service Worker] Install');
e.waitUntil((async () => {
//~ const cache = await caches.open(cacheName);
//~ console.log('[Service Worker] Caching all: app shell and content');
//~ await cache.addAll(contentToCache);
})());
});
self.addEventListener('fetch', (e) => {
e.respondWith((async () => {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetched resource ${e.request.url}`);
if (r) { return r; }
const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})());
});
Dear PWA Practitioners,
I'm learning PWA, at the caching part currently.
What is the purpose of defining cache items in install, where they can be done inside the fetch event as sampled in the above code?
It even looked more efficient because it only caches what's needed (I'm reading samples where - for example - all different sizes of icons are cached in the install part).
Thank you for the enlightenment!

Related

One time authentication in playwright is giving issues

I tried to have a one time authentication using session and use the same for all the tests in the spec file.
While trying to run my test , sometimes i get this below error which im unable to underdstand or fix. Any help on this would be appreciated.
browser.newContext: Cookie should have a valid expires, only -1 or a positive number for the unix timestamp in seconds is allowed
at C:\Users\v.shivarama.krishnan\source\repos\PlaywrightDemo\node_modules\#playwright\test\lib\index.js:595:23
at Object.context [as fn] (C:\Users\v.shivarama.krishnan\source\repos\PlaywrightDemo\node_modules\#playwright\test\lib\index.js:642:15)
Spec.ts
import { chromium, test, expect } from "#playwright/test";
test.describe('Launch Browser', () => {
await context.storageState({ path: 'storage/admin.json' });
await page.goto('abc.com');
await expect(page.locator('#ebiLink')).toBeVisible();
const texts = await page.locator('#ebiLink').textContent();
console.log("text of ebi is " + texts);
await page.goto('abc.com');
await expect(page.locator('text= Detailed Interfaces ')).toBeVisible();
await page.waitForSelector('#searchTab');
await page.waitForSelector('#InterfaceCard');
await page.locator('#searchTab').type('VISHW-I7939');
await page.locator("button[type='button']").click();
await page.locator('#InterfaceCard').first().click();
await expect(page.locator('#ngb-nav-0')).toBeVisible();
const interfaceID = await page.locator("//span[#class='value-text']").first().allInnerTexts();
console.log('interface id is' + interfaceID);
const dp = await page.waitForSelector('role=listbox');
await page.locator('role=listbox').click();
const listcount = await page.locator('role=option').count();
await page.locator('role=option').nth(1).click();
await expect(page.locator('#ngb-nav-0')).toBeVisible();
});
test('Move to shells screen', async ({ page, context }) => {
await context.storageState({ path: 'storage/admin.json' });
await page.goto('abc.com');
await expect(page.locator('#ListHeader')).toBeVisible();
const shells = await page.locator('#ListHeader').textContent();
console.log('Text of shells header is '+shells);
});
});
global-setup.ts (for one time login and getting the session)
import { Browser, chromium, FullConfig } from '#playwright/test'
async function globalSetup(config: FullConfig) {
const browser = await chromium.launch({
headless: false
});
await saveStorage(browser, 'Admin', 'User', 'storage/admin.json')
await browser.close()
}
async function saveStorage(browser: Browser, firstName: string, lastName: string, saveStoragePath: string) {
const page = await browser.newPage()
await page.goto('abc.com');
await page.waitForSelector("//input[#type='email']", { state: 'visible' });
await page.locator("//input[#type='email']").type('ABC#com');
await page.locator("//input[#type='submit']").click();
await page.locator("//input[#type='password']").type('(&^%');
await page.locator("//input[#type='submit']").click();
await page.locator('#idSIButton9').click();
await page.context().storageState({ path: saveStoragePath })
}
export default globalSetup
Have you registered global-setup.ts script in the Playwright configuration file: like below?
// playwright.config.ts
import { PlaywrightTestConfig } from '#playwright/test';
const config: PlaywrightTestConfig = {
globalSetup: require.resolve('./global-setup'),
};
export default config;
again you don't have to write code to use the session-storage at each test level, you can use - use attribute of Playwright configuration as below:
// playwright.config.ts
import { PlaywrightTestConfig } from '#playwright/test';
const config: PlaywrightTestConfig = {
globalSetup: require.resolve('./global-setup'),
use: {
// Tell all tests to load signed-in state from 'storageState.json'.
storageState: 'storageState.json'
}
};
export default config;
Seems like you are trying to use the same context in both tests, could that be a problem?
can you please try with isolated context and page for each tests?
Also please check if it make sense to use session storage at test level instead of context-
test.use({ storageState: './storage/admin.json' })
Update about the tests-
General structure of tests would be -
test.describe('New Todo', () => {
test('Test 1', async ({context, page }) => {});
test('Test 2', async ({context, page }) => {});
});
I looked into the source code of playwright and found these two lines which show the error message you see.
assert(!(c.expires && c.expires < 0 && c.expires !== -1), 'Cookie should have a valid expires, only -1 or a positive number for the unix timestamp in seconds is allowed');
assert(!(c.expires && c.expires > 0 && c.expires > kMaxCookieExpiresDateInSeconds), 'Cookie should have a valid expires, only -1 or a positive number for the unix timestamp in seconds is allowed');
The kMaxCookieExpiresDateInSeconds is defined as 253402300799.
So basically the cookie that you captured could breach one of above rules. In my case it's that the expiry of a cookie is greater than this figure :).
refer to source code - https://github.com/microsoft/playwright/blob/5fd6ce4de0ece202690875595aa8ea18e91d2326/packages/playwright-core/src/server/network.ts#L53

Cache storage works fine in chrome PWA but can't work in IOS pwa

I'm running vue.js app using vite and everything works fine in chrome/firefox browsers. also the basic functionality works fine on safari and IOS. I'm trying to access the cache storage and sync token between pwa app and safari browser.
Two weird things happens:
When user login to the website I set the cache and I simulate user behavior. for example a basic user login and then change the url to something else and back to the website. When this thing happens the caches variable is undefined...
When I set cache and user installs pwa there's no data stored in cache storage.
Note: I persist cache in local-storage of the browser and I know that I can't use local-storage to get token thus I use cache to share it between browser and pwa
Codes
How I cache token:
const CACHE_NAME = "auth";
const TOKEN_KEY = "token";
const FAKE_ENDPOINT = "/get-token";
export const saveToken = async (token: string) => {
try {
const cache = await caches.open(CACHE_NAME);
const responseBody = JSON.stringify({
[TOKEN_KEY]: token
});
const response = new Response(responseBody);
await cache.put(FAKE_ENDPOINT, response);
console.log("Token saved! 🎉");
console.log("Saved token: ", await getToken())
} catch (error) {
// It's up to you how you resolve the error
console.log("saveToken error:", {error});
}
};
export const getToken = async () => {
try {
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(FAKE_ENDPOINT);
if (!response) {
return null;
}
const responseBody = await response.json();
return responseBody[TOKEN_KEY];
} catch (error) {
// Gotta catch 'em all
console.log("getToken error:", {error});
}
};

Unit and integration test of Express REST API and multer single file update middleware

Introduction
Hello everybody,
I'm pretty new to unit and integration testing. The current REST API I'm working on involves file uploads and file system. If you want me to explain what's API this is, I can explain it to you using few sentences. Imagine a system like Microsoft Word. There are only users and users have documents. Users' documents are only JSON files and they are able to upload JSON file to add a document. My API currently has 3 routes, 2 middlewares.
Routes:
auth.js (authorization route)
documents.js (document centered CRUD operations)
users.js
Middlewares:
auth.js (To check if there is valid JSON web token to continue)
uploadFile.js (To upload single file using multer)
I have been able to unit/integration test auth.js, users.js routes and auth.js middleware. These routes and middlewares were only involving small packages of data I/O, so they were pretty easy for me. But documents.js router and uploadFile.js middleware is pretty hard for me to overcome.
Let me share my problems.
Source codes
documents.js Router
.
.
.
router.post('/mine', [auth, uploadFile], async (req, res) => {
const user = await User.findById(req.user._id);
user.leftDiskSpace(function(err, leftSpace) {
if(err) {
return res.status(400).send(createError(err.message, 400));
} else {
if(leftSpace < 0) {
fs.access(req.file.path, (err) => {
if(err) {
res.status(403).send(createError('Your plan\'s disk space is exceeded.', 403));
} else {
fs.unlink(req.file.path, (err) => {
if(err) res.status(500).send('Silinmek istenen doküman diskten silinemedi.');
else res.status(403).send(createError('Your plan\'s disk space is exceeded.', 403));
});
}
});
} else {
let document = new Document({
filename: req.file.filename,
path: `/uploads/${req.user.username}/${req.file.filename}`,
size: req.file.size
});
document.save()
.then((savedDocument) => {
user.documents.push(savedDocument._id);
user.save()
.then(() => res.send(savedDocument));
});
}
}
});
});
.
.
.
uploadFile.js Middleware
const fs = require('fs');
const path = require('path');
const createError = require('./../helpers/createError');
const jsonFileFilter = require('./../helpers/jsonFileFilter');
const multer = require('multer');
const storage = multer.diskStorage({
destination: function(req, file, cb) {
console.log('file: ', file);
if(!req.user.username) return cb(new Error('Dokümanın yükleneceği klasör için isim belirtilmemiş.'), null);
let uploadDestination = path.join(process.cwd(), 'uploads', req.user.username);
fs.access(uploadDestination, (err) => {
if(err) {
// Directory with username doesn't exist in uploads folder, so create one
fs.mkdir(uploadDestination, (err) => {
if(err) cb(err, null);
cb(null, uploadDestination);
});
} else {
// Directory with username exists
cb(null, uploadDestination);
}
});
},
filename: function(req, file, cb) {
cb(null, `${file.originalname.replace('.json', '')}--${Date.now()}.json`);
}
});
module.exports = function(req, res, next) {
multer({ storage: storage, fileFilter: jsonFileFilter }).single('document')(req, res, function(err) {
if(req.fileValidationError) return res.status(400).send(createError(req.fileValidationError.message, 400));
else if(!req.file) return res.status(400).send(createError('Herhangi bir doküman seçilmedi.', 400));
else if(err instanceof multer.MulterError) return res.status(500).send(createError(err.message, 500));
else if(err) return res.status(500).send(createError(err, 500));
else next();
});
}
Questions
1. How can I test user.leftDiskSpace(function(err, leftSpace) { ... }); function which has a callback and contains some Node.js fs methods which also has callbacks?
I want to reach branches and statements user.leftDiskSpace() function containing. I thought of using mock functions to mock out the function but I don't know how to do so.
2. How to change multer disk storage's upload destination for a specified testing folder?
Currently my API uploads the test documents to development/production uploads disk storage destination. What is the best way to change upload destination for testing? I thought to use NODE_ENV global variable to check if the API is being tested or not and change destination in uploadFile.js middleware but I'm not sure if it's a good solution of this problem. What should I do?
Current documents.test.js file
const request = require('supertest');
const { Document } = require('../../../models/document');
const { User } = require('../../../models/user');
const mongoose = require('mongoose');
const path = require('path');
let server;
describe('/api/documents', () => {
beforeEach(() => { server = require('../../../bin/www'); });
afterEach(async () => {
server.close();
await User.deleteMany({});
await Document.deleteMany({});
});
.
.
.
describe('POST /mine', () => {
let user;
let token;
let file;
const exec = async () => {
return await request(server)
.post('/api/documents/mine')
.set('x-auth-token', token)
.attach('document', file);
}
beforeEach(async () => {
user = new User({
username: 'user',
password: '1234'
});
await user.save();
token = user.generateAuthToken();
file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.json');
});
it('should return 400 if no documents attached', async () => {
file = undefined;
const res = await exec();
expect(res.status).toBe(400);
});
it('should return 400 if a non-JSON document attached', async () => {
file = path.join(process.cwd(), 'tests', 'integration', 'files', 'test.png');
const res = await exec();
expect(res.status).toBe(400);
});
});
});

access document.documentElement from puppeteer

I can get access to the entire HTML for any URL by opening dev-tools and typing:
document.documentElement
I am trying to replicate the same behavior using puppeteer, however, the snippet below returns {}
const puppeteer = require('puppeteer'); // v 1.1.0
const iPhone = puppeteer.devices['Pixel 2 XL'];
async function start(canonical_url) {
const browserURL = 'http://127.0.0.1:9222';
const browser = await puppeteer.connect({browserURL});
const page = await browser.newPage();
await page.emulate(iPhone);
await page.goto(canonical_url, {
waitUntil: 'networkidle2',
});
const data = await page.evaluate(() => document.documentElement);
console.log(data);
}
returns:
{}
Any idea on what I could be doing wrong here?

Puppeteer Generate PDF from multiple HTML strings

I am using Puppeteer to generate PDF files from HTML strings.
Reading the documentation, I found two ways of generating the PDF files:
First, passing an url and call the goto method as follows:
page.goto('https://example.com');
page.pdf({format: 'A4'});
The second one, which is my case, calling the method setContent as follows:
page.setContent('<p>Hello, world!</p>');
page.pdf({format: 'A4'});
The thing is that I have 3 different HTML strings that are sent from the client and I want to generate a single PDF file with 3 pages (in case I have 3 HTML strings).
I wonder if there exists a way of doing this with Puppeteer? I accept other suggestions, but I need to use chrome-headless.
I was able to do this by doing the following:
Generate 3 different PDFs with puppeteer. You have the option of saving the file locally or to store it in a variable.
I saved the files locally, because all the PDF Merge plugins that I found only accept URLs and they don't accept buffers for instance. After generating synchronously the PDFs locally, I merged them using PDF Easy Merge.
The code is like this:
const page1 = '<h1>HTML from page1</h1>';
const page2 = '<h1>HTML from page2</h1>';
const page3 = '<h1>HTML from page3</h1>';
const browser = await puppeteer.launch();
const tab = await browser.newPage();
await tab.setContent(page1);
await tab.pdf({ path: './page1.pdf' });
await tab.setContent(page2);
await tab.pdf({ path: './page2.pdf' });
await tab.setContent(page3);
await tab.pdf({ path: './page3.pdf' });
await browser.close();
pdfMerge([
'./page1.pdf',
'./page2.pdf',
'./page3.pdf',
],
path.join(__dirname, `./mergedFile.pdf`), async (err) => {
if (err) return console.log(err);
console.log('Successfully merged!');
})
I was able to generate multiple PDF from multiple URLs from below code:
package.json
{
............
............
"dependencies": {
"puppeteer": "^1.1.1",
"easy-pdf-merge": "0.1.3"
}
..............
..............
}
index.js
const puppeteer = require('puppeteer');
const merge = require('easy-pdf-merge');
var pdfUrls = ["http://www.google.com","http://www.yahoo.com"];
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
var pdfFiles=[];
for(var i=0; i<pdfUrls.length; i++){
await page.goto(pdfUrls[i], {waitUntil: 'networkidle2'});
var pdfFileName = 'sample'+(i+1)+'.pdf';
pdfFiles.push(pdfFileName);
await page.pdf({path: pdfFileName, format: 'A4'});
}
await browser.close();
await mergeMultiplePDF(pdfFiles);
})();
const mergeMultiplePDF = (pdfFiles) => {
return new Promise((resolve, reject) => {
merge(pdfFiles,'samplefinal.pdf',function(err){
if(err){
console.log(err);
reject(err)
}
console.log('Success');
resolve()
});
});
};
RUN Command: node index.js
pdf-merger-js is another option. page.setContent should work just the same as a drop-in replacement for page.goto below:
const PDFMerger = require("pdf-merger-js"); // 3.4.0
const puppeteer = require("puppeteer"); // 14.1.1
const urls = [
"https://news.ycombinator.com",
"https://en.wikipedia.org",
"https://www.example.com",
// ...
];
const filename = "merged.pdf";
let browser;
(async () => {
browser = await puppeteer.launch();
const [page] = await browser.pages();
const merger = new PDFMerger();
for (const url of urls) {
await page.goto(url);
merger.add(await page.pdf());
}
await merger.save(filename);
})()
.catch(err => console.error(err))
.finally(() => browser?.close())
;