How to ignore that element doesn't exist and skip taking a screenshot? - playwright

I would like to take a screenshot of an element that might or might not exist
const elem = await page.locator('.selector')
await elem.scrollIntoViewIfNeeded();
await elem.screenshot({path: 'screenshot.png'});
If the element isn't on the page this results in
elementHandle.screenshot: Timeout 30000ms exceeded.
Is there a way to just ignore the nonexistence and move on?
This behaves the same
const elem = await page.locator('.selector')
if(elem) {
await elem.scrollIntoViewIfNeeded();
await elem.screenshot({path: 'screenshot.png'});
}

Try wrapping your logic in try {} catch() {} as the possible throw created by screenshot not existing is going to be passed to catch where it can be ignored, instead crashing runtime.

Related

Locating optional element in Playwright

I'm using Playwright for web scraping and I currently need to find a certain description text. I know there doesn't have to be a description text on each page I scrape from that website so I want it to be "optional".
I've solved it and it works but I think it's ugly and I want to know if there is a better way?
let description: string | undefined;
try {
const locator = page.locator(".svtmat_recipe__preamble");
await locator.waitFor({ timeout: 3000 });
description = (await locator.textContent()) ?? undefined;
} catch {}
Your code looks good, but I would change it a little bit:
// Because 'textContent' method returns promise of string | null.
// So you don't need to use the 'nullish coalescing' operator (??)
let description: string | null;
try {
const locator = page.locator(".svtmat_recipe__preamble");
// also you can pass { timeout: 3000 } to the 'textContent' method to wait for it
description = await locator.textContent({ timeout: 3000 })
} catch {}
It depends, weather you need to wait "some time" for that locator to be present in the DOM.
await page.isVisible("text='yourText'")
or
if(await page.locator(your_selector).count()>0)
But in case you need to wait for element to be present in DOM, try catch seems to be one of the appropriate solutions, since waitForSelector is throwing exepction if timeout is reached.
Remember locator will not wait for any selector to appear. So you might need to await for the result.
const selector = '.svtmat_recipe__preamble'
await page.waitForSelector(selector)
const el = await page.locator(foo).count()
console.log(el)

Flutter: flutter_downloader SQL not working properly

I followed the setup instructions exactly to set up flutter_downloader.
I'm starting downloads from a method like so, which does show progress in the callback (exactly as in the docs):
Future<String?> startDownload(String url) async {
Directory directory = await getApplicationDocumentsDirectory();
final taskId = await FlutterDownloader.enqueue(
url: url,
savedDir: directory.path,
showNotification: true, // Android only
openFileFromNotification: false, // Android only
);
return taskId;
}
Yet, calling the method as described in the docs:
List<DownloadTask>? tasks = await FlutterDownloader.loadTasks();
Often results in zero results, even though you have started a download prior.
This seems completely random.
I've also tried with a query with the same result, zero items. e.g.
List<DownloadTask>? tasks = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task",
);
Anything I'm doing wrong?

Firestore batch.commit adding more than 500 documents at a time

I am new to Firestore, trying to figure out a fast way to add some documents in Firestore using Dart.
I used the code below. I had about 3000 strings in a list, and it ended up adding all the 3000 documents, but it took a long time, about 10 minutes, and I also got an error message after batch.commit, that the 500 limit was exceeded, even though it added all 3000 documents.
I know I should break it down into 500 at a time, but the fact that it added all the documents in the list does not make sense to me. I checked in Firestore Console, and all the 3000 documents were there.
I need to create a document id every time I add a document. What am I doing wrong? Is it ok to use the add to get a document reference, and then batch.setData?
Future<void> addBulk(List<String> stringList) async {
WriteBatch batch = Firestore.instance.batch();
for (String str in stringList) {
// Check if str already exists, if it does not exist, add it to Firestore
if (str.isNotEmpty) {
QuerySnapshot querySnapshot = await myCollection
.where('name', isEqualTo: str)
.getDocuments(source: Source.cache);
if (querySnapshot.documents.length == 0) {
UserObject obj = UserObject(name: str);
DocumentReference ref = await myCollection.add(obj.toMap());
batch.setData(ref, UserObject.toMap(), merge: true);
}
}
}
batch.commit();
}
Your code is actually just adding each document separately, regardless of what you're doing with the batch. This line of code does it:
DocumentReference ref = await myCollection.add(obj.toMap());
If you remove that line (which, again, is not touching your batch), I'm sure you will just see a failure due to the batch size.
If you are just trying to generate a unique ID for the document to be added in the batch, use document() with no parameters instead:
DocumentReference ref = myCollection.document();

In Dart, why waiting for a trivial ("empty") StreamController stream immediately exits program?

This is the backbone of a simple program that uses a StreamController to listen on some input stream and outputs some other data on its own stream as a reaction:
import 'dart:async';
main() async {
var c = StreamController(
onListen: (){},
onPause: (){},
onResume: (){},
onCancel: (){});
print("start");
await for (var data in c.stream) {
print("loop");
}
print("after loop");
}
Output:
$ dart cont.dart
start
$ dart
Why does this code exit immediately at the await for line without executing neither print("loop") nor print("after loop") ?
Note: in the original program, onListen() will receive the input stream and subscribe to it. The loop actually works until calling subscription.cancel() on the input stream, when it also suddenly exits, without any chance to clean up.
I was surprised by this, but then again all of async/await code is converted to .then()s and it seems everything can't be perfectly translated.
It seems this is part of the answer: You need to close the stream in another task to get out of the await for. Here is a previous related question. Since we don't close it, lines after await for do not execute. Here's an example that avoids this:
import 'dart:async';
main() async {
var c = StreamController(
onListen: () {},
onPause: () {},
onResume: () {},
onCancel: () {});
print("start");
await Future.wait(<Future>[producer(c), consumer(c)]);
print("after loop");
}
Future producer(StreamController c) async {
// this makes it print "loop" as well
// c.add("val!");
await c.close();
}
Future consumer(StreamController c) async {
await for (var data in c.stream) {
print("loop");
}
}
which prints:
start
after loop
So, moral of the story is,
Never leave a StreamController unattended or weird things will happen.
Some async functions that you wait on will break your code, even bypassing your try/finally block!
I thought I knew async/await inside out, but this second point scares me now.

Service Workers and IndexedDB

In a simple JavaScript Service Worker I want to intercept a request and read a value from IndexedDB before the event.respondWith
But the asynchronous nature of IndexDB does not seem to allow this.
Since the indexedDB.open is asynchronous, we have to await it which is fine. However, the callback (onsuccess) happens later so the function will exit immediately after the await on open.
The only way I have found to get it to work reliably is to add:
var wait = ms => new Promise((r, j) => setTimeout(r, ms));
await wait(50)
at the end of my readDB function to force a wait until the onsuccess has completed.
This is completely stupid!
And please don't even try to tell me about promises. They DO NOT WORK in this circumstance.
Does anyone know how we are supposed to use this properly?
Sample readDB is here (all error checking removed for clarity). Note, we cannot use await inside the onsuccess so the two inner IndexedDB calls are not awaited!
async function readDB(dbname, storeName, id) {
var result;
var request = await indexedDB.open(dbname, 1); //indexedDB.open is an asynchronous function
request.onsuccess = function (event) {
let db = event.target.result;
var transaction = db.transaction([storeName], "readonly"); //This is also asynchronous and needs await
var store = transaction.objectStore(storeName);
var objectStoreRequest = store.get(id); //This is also asynchronous and needs await
objectStoreRequest.onsuccess = function (event) {
result = objectStoreRequest.result;
};
};
//Without this wait, this function returns BEFORE the onsuccess has completed
console.warn('ABOUT TO WAIT');
var wait = ms => new Promise((r, j) => setTimeout(r, ms));
await wait(50)
console.warn('WAIT DONE');
return result;
}
And please don't even try to tell me about promises. They DO NOT WORK in this circumstance.
...
...
...
I mean, they do, though. Assuming that you're okay putting the promise-based IndexedDB lookups inside of event.respondWith() rather than before event.respondWith(), at least. (If you're trying to do this before calling event.respondWith(), to figure out whether or not you want to respond at all, you're correct in that it's not possible, since the decision as to whether or not to call event.respondWith() needs to be made synchronously.)
It's not easy to wrap IndexedDB in a promise-based interface, but https://github.com/jakearchibald/idb has already done the hard work, and it works quite well inside of a service worker. Moreover, https://github.com/jakearchibald/idb-keyval makes it even easier to do this sort of thing if you just need a single key/value pair, rather than the full IndexedDB feature set.
Here's an example, assuming you're okay with idb-keyval:
importScripts('https://cdn.jsdelivr.net/npm/idb-keyval#3/dist/idb-keyval-iife.min.js');
// Call idbKeyval.set() to save data to your datastore in the `install` handler,
// in the context of your `window`, etc.
self.addEventListener('fetch', event => {
// Optionally, add in some *synchronous* criteria here that examines event.request
// and only calls event.respondWith() if this `fetch` handler can respond.
event.respondWith(async function() {
const id = someLogicToCalculateAnId();
const value = await idbKeyval.get(id);
// You now can use `value` however you want.
const response = generateResponseFromValue(value);
return response;
}())
});

Resources