Puppeteer (Google Headless Chrome Node API) ile arayüz testi yazmak

Bir süredir bu konu ile alakalı bir şeyler denemek ve karalamak istiyordum fakat fırsat bulamamıştım.  Puppeteer, google tarafından geliştirilen chrome’u headless (herhangi bir kullanıcı arayüzü olmadan) olarak yönetebileceğiniz api sunan bir nodejs kütüphanesidir.

puppeteer

Tarayıcınızda manuel olarak yapabildiğiniz hemen her şeyi puppeteer ile otomatikleştirebilirsiniz.

Bir kaç örnek vermek gerekirse;

  • Sayfaların istediğiniz çözünürlükte ekran görüntülerini alabilir ve pdf olarak kaydedebilir,
  • Web sayfalarından programatik şekilde veri çekebilir,
  • Her deploy öncesi yapılan sıkıcı son kullanıcı testlerinizi otomatikleştirebilirsiniz…

Bu yazıda basit bir todo (https://github.com/irfansimsar/es6-todo-app) uygulaması üzerinden son kullanıcı testlerini nasıl otomatikleştirebileceğimize bakacağız. Daha önce bu tip bir işle uğraştıysanız muhtemelen phantomjs ve selenium gibi diğer popüler alternatiflerle çalışmışsınızdır.  Puppeteer’ın en sevdiğim yanı çok kolay öğrenilebilir ve hızlı şekilde kullanılabiliyor olması.

Puppeteer’ı aşağıdaki şekilde başka bir eklentiye gerek duymadan aşağıdaki gibi kullanabiliriz.

import puppeteer from 'puppeteer';

(async () => {
  // Puppeteer ile browser'ı aç
  const browser = await puppeteer.launch();

  // Browser'da yeni bir sayfa aç
  const page = await browser.newPage();

  // Test edilecek sayfaya git
  await page.goto('http://loremipsum.com');

  // Placeholder'ı "Kullanıcı adı" olan inputu görene kadar bekle
  await page.waitForSelector('input[placeholder="Kullanıcı adı"]');

  // Placeholder'ı "Kullanıcı adı" olan input'a kullanıcı adını gir
  await page.type('input[placeholder="Kullanıcı adı"]', 'lorem');

  // Placeholder'ı "Şifre" olan input'a şifreyi gir
  await page.type('input[placeholder="Şifre"]', "ipsum");

  // "submit-button" class'ına sahip olan butona tıkla
  await page.click('.submit-button');

  // 2 saniye bekle
  await page.waitFor(2000);

  // Giriş yapıldı mı kontrolü yap ve console'a bas
  await page.evaluate(() => {
    if (page.$('input[placeholder="Kullanıcı adı"]')) {
      console.log('Giriş yapılamadı');
    } else {
      console.log('Giriş yapıldı');
    }
  });

  // Açılan sayfanın ekran görüntüsünü al example.png olarak kaydet
  await page.screenshot({path: 'example.png'});

  // Tarayıcıyı kapat
  await browser.close();
})();

Fakat biz bu örnekte biraz daha gerçek dünya problemi çözdüğümüzü düşünerek Jest (Facebook’un geliştirdiği test framework’ü) ve Puppeteer’ı birlikte kullanacağız.

Koda girişmeden önce projemizin (https://irfansimsar.github.io/es6-todo-app/) ne iş yaptığına bakalım ve basit olarak bir kaç test case’i yazalım.

– Kullanıcı sayfadaki input field’ına minimum 3 karakterden oluşacak şekilde görev girer enter’a basar ve girdiği görevi input field’ının altındaki listede görür.

– Daha önce oluşturduğu görev üzerindeki “👌” butonuna tıklayarak ilgili görevi tamamlandı olarak işaretleyebilir. Tamamlanmış görev listede üzeri çizili şekilde görünür.

– Daha önce oluşturduğu görev üzerindeki “👎” butonuna tıklayarak ilgili görevi silebilir. Silinen görev listeden kalkar.

  1. Öncelikle terminal’imizi açıyoruz ve testlerimizi yazacağımız bir test klasörü oluşturuyoruz ve içerisine giriyoruz.
    $ mkdir test
    $ cd test
  2. Npm üzerinden puppeteer ve jest’i yüklüyoruz.
    $ npm i puppeteer jest --save-dev
    
  3. Test folder’ı içerisine testlerimizi yazacağımız js dosyasını oluşturuyoruz.
    $ mkdir test
    $ touch test/uitest.spec.js
    
    
  4. Ve test kodumuzu yazıyoruz
    const puppeteer = require('puppeteer');
    
    const appUrl = 'https://irfansimsar.github.io/es6-todo-app';
    let page;
    let browser;
    const width = 800;
    const height = 600;
    const testTasks = [
      "Lorem ipsum",
      "do"
    ];
    
    beforeAll(async () => {
      browser = await puppeteer.launch({
        args: [`--window-size=${width},${height}`]
      });
      page = await browser.newPage();
      await page.setViewport({ width, height });
      await page.goto(appUrl);
    });
    
    afterAll(() => {
      browser.close();
    });
    
    describe('Add new task', () => {
      test('user can add new task minimum 3 char', async () => {
        await page.waitForSelector('input[placeholder="please enter the task..."]');
        const inputElem = await page.$('input[placeholder="please enter the task..."]');
        const beforeTodoCount = await page.$$eval('.todos li', li => li.length);
        await inputElem.type(testTasks[0]);
        await inputElem.press('Enter');
        const afterTodoCount = await page.$$eval('.todos li', li => li.length);
    
        expect(afterTodoCount).toBeGreaterThan(beforeTodoCount);
      });
    
      test('user can not add new task if character length less than 3', async () => {
        await page.waitForSelector('input[placeholder="please enter the task..."]');
        const inputElem = await page.$('input[placeholder="please enter the task..."]');
        const beforeTodoCount = await page.$$eval('.todos li', li => li.length);
        await inputElem.type(testTasks[1]);
        await inputElem.press('Enter');
        const afterTodoCount = await page.$$eval('.todos li', li => li.length);
    
        expect(afterTodoCount).toBe(beforeTodoCount);
      });
    });
    
    describe('Mark a task as completed', () => {
      test('user can mark a task as completed', async () => {
        const btnDone = await page.$('.todos li .btn-done');
        await btnDone.click();
        const doneCount = await page.$$eval('.todos li.done', li => li.length);
        
        expect(doneCount).toBe(1);
      });
    });
    
    
    describe('Delete a task', () => {
      test('user can delete a task', async () => {
        const btnDone = await page.$('.todos li .btn-delete');
        await btnDone.click();
        const afterTodoCount = await page.$$eval('.todos li', li => li.length);
    
        expect(afterTodoCount).toBe(0);
      });
    });
    

Daha detaylı bilgi ve örnekler için github sayfasını inceleyebilirsiniz.

 

Nedir bu Arrow Function (ES6)

es6 arrow function

EcmaScript 6’nın işimizi kolaylaştıran güzelliklerinden bir diğeri ise Arrow Function.  Arrow function javascript’de fonksiyon tanımlamanın diğer bir yolu olmasına rağmen standart fonksiyon yazımına göre çok daha az satırda işimizi halletmemize olanak tanıyor.

Bir kaç örnekle nasıl kullanabileceğimize bakalım

// kişiler adında bir array'imiz var
const kisiler = [{name: 'ali', age: 30}, {name: 'veli', age: 5}, {name: 'ayşe', age: 9}]

// Bu kişilerin sadece isimlerini ekrana yazdırmak istedeydik standart fonksiyon tanımıyla
const isimler = kisiler.map(function (kisi) {
  return kisi.name
})

console.log(isimler) // konsola ["ali", "veli", "ayşe"] yazar

// Şimdi bu işi arrow function ile yapalım ve kişilerin yaşlarını konsola yazdıralım
const yaslar = kisiler.map((kisi) => {
  return kisi.age
})

console.log(yaslar) // konsola [30, 5, 9] yazar

// Eğer fonksiyonunuz sadece tek satırdan oluşuyor ve bu satırı return ediyorsa süslü parantez kullanmanıza gerek yoktur
// Eğer fonksiyonunuz sadece tek bir parametre alıyorsa parantez kullanmanıza da gerek yoktur.
// Yaşı 18'den küçük olan kişileri filtreleyelim
const cocuklar =  kisiler.filter(kisi => kisi.age < 18)

// Çocukların isimlerini yaşları ile birlikte ekrana yazdıralım.
const cocuklarIsımYas = cocuklar.map(cocuk => `${cocuk.name} ${cocuk.age} yaşındadır.`)
console.log(cocuklarIsımYas)

EcmaScript 6 öncesi bir fonksiyon içerisinde başka bir fonksiyon kullandığımız zaman eğer this keyword’üne ihtiyacımız varsa this keyword’ünü önce self ya da that gibi bir isim verdiğimiz bir değişkene atıyorduk ki bir alt fonksiyonda da bu kapsamdaki this’e erişebilelim. Örnekle açıklamak gerekirse

// sayfada bir butonumuz var ve tıkladığımızda önce active class'ı ekliyoruz ve belirli bir süre sonra active class'ını kaldırıyoruz. Aşağıdaki örnekte setTimeout fonksiyonu içerisindeki this window'a tekabül ettiğinden butona eklenen class kaldırılamaz

<button class="my-button">My Button</button>

var button = document.querySelector('.my-button')
button.addEventListener('click', function () {
  console.log(this) // this bu aşamada buton'un kendisidir.
  this.classList.add('active')

  setTimeout(function () {
    console.log(this) // this bu aşamada Window objesine tekabül eder
    this.classList.remove('active') // çalışmaz
  }, 500)
})

// Bu yüzden this'i önce başka bir değişkene atamamız gerekiyor

<button class="my-button">My Button</button>

var button = document.querySelector('.my-button')
button.addEventListener('click', function () {
  var that = this
  this.classList.add('active')

  setTimeout(function () {
    console.log(that) // artık that ile setTimeout fonksiyonunun içerisinde de butona erişebiliyoruz
    that.classList.remove('active') // çalışır
  }, 500)
})

// arrow function kullandığımızda bu tip bir arkadan dolanmaya gerek kalmıyor.
<button class="my-button">My Button</button>

var button = document.querySelector('.my-button')
button.addEventListener('click', function () {
  this.classList.add('active')

  setTimeout(() => {
    console.log(this) // artık this ile setTimeout fonksiyonunun içerisinde de butona erişebiliyoruz
    this.classList.remove('active') // çalışır
  }, 500)
})

 

ES6 Yeni String Metodları

ES6 Yeni String Metodları

ES6 genel olarak hem daha az ve düzenli hem de daha okunabilir kod yazmamıza olanak sağlıyor. Bu yazıda String tipinde kullanabileceğimiz dört yeni metod (includes(), startsWith(), endsWith(), repeat() ) üzerinde duracağız.

includes, startWith ve endsWith metodları isimlerinden de anlaşılabileceği gibi bir string içersinde arama yapmak için kullanılıyorlar. Ve bu üç metod da case-sensitive (büyük – küçük harfe duyarlı)

Örnekler üzerinden inceleyecek olursak;

.includes()

String’in verilen arama kelimesi içerip içermediğini kontrol eder.

// Öncelikle üzerinde çalışacağımız string tipindeki değişkenleri tanımlıyoruz

const konu = 'Bu günkü konumuz ES6 String İşlemleri'
const tarih = '01 Aralık 2017'



// Case Sensitive (Büyük - Küçük harfe duyarlı)
console.log(konu.includes('ES6')) // true döner
console.log(konu.includes('es6')) // false döner

// Parametre olarak başlangıç pozisyonunu alır

console.log(tarih.includes('Aralık')) // true döner
console.log(tarih.includes('Aralık', 4)) // false döner




.startsWith()

String’in verilen arama kelimesiyle başlayıp başlamadığını kontrol eder. Parametre olarak hangi karakterden sonra başladığı kontrol edilebilir.

// Öncelikle üzerinde çalışacağımız string tipindeki değişkenleri tanımlıyoruz

const konu = 'ES6 String İşlemleri'
const tarih = '01 Aralık 2017'

// Case Sensitive (Büyük - Küçük harfe duyarlı)
console.log(konu.startsWith('es6')) // false döner
console.log(konu.startsWith('ES6')) // true döner


// Parametre olarak başlangıç pozisyonu alır

console.log(tarih.startsWith('Aralık')) // false döner
console.log(tarih.startsWith('Aralık', 3)) // true döner

.endsWith()

startsWith() metoduyla benzer olarak çalışır string’in verilen arama kelimesiyle bitip bitmediğini kontrol eder.

// Öncelikle üzerinde çalışacağımız string tipindeki değişkenleri tanımlıyoruz

const konu = 'ES6 String İşlemleri'
const tarih = '01/12/2017 Cuma'


// Case Sensitive (Büyük - Küçük harfe duyarlı)

console.log(konu.endsWith('işlemleri')) // false döner
console.log(konu.endsWith('İşlemleri')) // true döner


// Parametre olarak length (karakter uzunluğu) alır, örneğin aşağıdaki kullanımda tarih değişkenindeki ilk 10 karakter 2017 ile mi bitiyor diye bakıyoruz

console.log(date.endsWith('2017', 10)) // true döner

.repeat()

String’i verilen sayı kadar tekrar eder.

// Öncelikle üzerinde çalışacağımız string tipindeki değişkenleri tanımlıyoruz

const halayBasi = '¯_(ツ)_/¯'
const halayci = '¯_(-_-)_¯'

console.log(halayci.repeat(10)) // halay başsız olmaz
console.log(`${halayci.repeat(10)}${halayBasi}`) // halay başı mutludur

 

ES6 Değişkenler (var, let, const)

ES6 Değişkenler

ES6 öncesi değişken tanımlamak için sadece var keyword’ü kullanılıyorken, ES6 ile birlikte hayatımıza let ve const keyword’leri girdi. Her ne kadar temelde bu üç keyword’ün görevi de değişken tanımlamak olsa da aslında her biri farklı ihtiyaçlara cevap veriyor.

ES6 henüz tüm browser’larda desteklenmediğinden ES6 ile yazılmış kodların düzgün çalışabilmesi için babel benzeri transpile tool’larına ihtiyaç var.

Örnekler üzerinden bu keyword’leri inceleyecek olursak;

var

var keyword’ü aralarında en geniş kapsama sahip keyword’dür. Örneklerle inceleyecek olursak

  • Sonradan tekrar değiştirilebilir
  • Kodun herhangi bir noktasında tekrar tekrar tanımlanabilir.
  • Sadece fonksion ile kapsanabilir (function scope) fonksiyon süslü parantezleri içerisinde var ile tanımlanan değişkene dışarıdan erişilemez.
var isim = 'irfan';

console.log(isim); // "irfan" döner

isim = 'simsar'; // sonradan değiştirilebilir

var isim = 'deneme'; // tekrar tekrar tanımlanabilir

function yasiAyarla (_yas) {
  var yas = _yas;
  console.log(yas);
}

yasiAyarla(30);

console.log(yas); // undefined döner

 let

let keyword’ü sadece tanımlandığı kapsam (block scope) içerisinden erişilebilir. Block scope’dan kasıt her bir süslü parantezin ({}) içerisidir

  • Sonradan tekrar değiştirilebilir
  • Aynı kapsam (scope) içerisinde sadece bir sefer tanımlanabilir. Tekrar tanımlanmaya çalıştığında kod hata verir ve çalışmayı durdurur.
let isim = 'irfan';

console.log(isim); // "irfan" döner

isim = 'simsar'; // sonradan değiştirilebilir

let isim = 'deneme'; // Uncaught SyntaxError: Identifier 'isim' has already been declared hatası döner ve kod çalışmayı durdurur

function yasiAyarla (_yas) {
  let yas = _yas;
  console.log(yas); // 30 döner
}

yasiAyarla(30);

console.log(yas); // undefined döner

for (let i = 0; i < 10; i++;) {
  console.log(i); // 0'dan 9'a kadar i ekrana basılır
}

console.log(i) // undefined döner

const

const keyword’ü de let gibi sadece tanımlandığı kapsam (block scope) içerisinden erişilebilir. const adından da anlaşılabileceği gibi (constant kelimesinin kısaltılmasıdır) sabittir.

  • Sonradan değiştirilemez
  • Aynı kapsam (scope) içerisinde sadece bir sefer tanımlanabilir. Tekrar tanımlanmaya çalıştığında kod hata verir ve çalışmayı durdurur
const isim = 'irfan';

console.log(isim); // "irfan" döner

isim = 'simsar'; // Uncaught TypeError: Assignment to constant variable. hatası döner ve kod çalışmayı durdurur.
{
  const soyisim = 'simsar';
  console.log(soyisim); // "simsar" döner
}

console.log(soyisim); // undefined döner

const kisi = {
  isim: 'irfan',
  soyisim: 'simsar',
  yas: 30
};

kisi = { 
  isim: 'irfan',
  soyisim: 'simsar',
  yas: 30
}; // Uncaught TypeError: Assignment to constant variable. hatası döner ve kod çalışmayı durdurur.

kisi.yas = 31;

console.log(kisi); // { isim: 'irfan', soyisim: 'simsar', yas: 31 } döner

const haftasonu = ['Cumartesi', 'Pazar'];

haftasonu = ['Cumartesi', 'Pazar', 'Pazartesi']; Uncaught TypeError: Assignment to constant variable.

haftasonu.push('Pazartesi'); // Hata vermez (içten içe ne güzel olmaz mı der)

console.log(haftasonu) // ["Cumartesi", "Pazar", "Pazartesi"] döner

 

  • Yukarıdaki örnekte de görüldüğü gibi eğer Object ve Array bu tiplerin mutable olmasından dolayı const keyword’ü ile tanımlansa da tamamen değiştirilememelerine rağmen özellikleri manipüle edilebilir. mutable ve unmutable tipler ile ilgili “İpin ucunu kaçırmamak” başlıklı yazı bu konu ile ilgili kafayı baya açıyor.

Hangi keyword’ü ne zaman kullanmalıyız?

Javascript uygulamanızın kapsamı büyüdükçe tanımlanan değişkenler daha sonra projede çalışan başkaları tarafından tekrar tanımlanmak istenebilir ve memory leak’e neden olabilir. let ve const keyword’leri bu kapsamda imdadımıza koşuyor. Değişkenlerin kapsamını ne kadar daraltırsak sızıntı da o ölçüde azalıyor.

Keywordlerin kullanımıyla ilgili farklı farklı görüşler olsa da bu konuda Mathias Bynens’in görüşü bana daha mantıklı geliyor.

  • Mümkün olduğunca const kullan
  • Sadece değişkenin değerini değiştirme ihtiyacın var ise let kullan
  • Eğer ES6 yazıyorsan var kullanma

Radyo Tiyatrosu

Her ne kadar günümüzde radyonun eski günlerdeki tadı ve coşkusu kalmamış olsa da, ekran bombardımanına maruz kalamadığımız durumlarda radyo dinlemeye devam ediyoruz.

radyo tiyatrosu

Radyonun tahtı televizyon, bilgisayar derken şimdi de telefonun insanları meşgul etmesiyle birlikte iyiden iyiye sarsılmış durumda. Aslında sarsılan sadece radyonun tahtından ziyade insan ilişkileri fakat bu başka bir yazının konusu olacak kadar derin bir mevzu.

En azından İstanbul trafiğinin o insana hayatı sorgulatan, yaşadığı şehirden ve belki de her biri kendi içinde çok güzel olan insanlardan nefret etmesine neden olan keşmekeşi içerisinde radyo hala bir kurtarıcı olabiliyor.

radyo

Geçenlerde Youtube’da videodan videoya gezerken bir Radyo Tiyatrosu kaydına denk geldim ve sonrasında bir çoğunu dinledim. Kullanılan Türkçe ve yapılan seslendirmeler muazzam. İnsan dinlerken an geliyor bulunduğu ortamdan uzaklaşıp hikayenin içine giriveriyor.

Günümüzde ekranlarda ve diğer iletişim kanallarında kullanılan dil çoğunlukla o kadar sığ ve bayağı ki, insan eski bir şeyler izlediğinde ya da dinlediğinde bu ayrımın daha bir farkına varıyor.

Örneğin Ankara Radyosu yapımı olan Müşerref Öztürk’ün yazıp Rüştü Asyalı’nın yönettiği “Bir Cuma Hikayesi” gecenin bu saatlerinde dinlendiğinde insanı garip bir hüzne sürüklüyor, yalnızlık zor zanaat.

Şener Şen, Suna Pekuysal, Zihni Göktay gibi sanatçıların seslendirdiği bir Alman hikayesi olan “Gece Treni” de gayet başarılı ve ülkesinden göç etmek zorunda kalanların arttığı günümüzde konusu ilgi çekici.

“Radyo’nun geleceği yok!”
Lord Kelvin (1898, İskoçyalı bilim insanı)