Test events example including Uninstall events

From ISXKB

Jump to: navigation, search

This example is a copy of an article in the old ISXKB. It logs all Setup and Uninstall events into the file C:\Log_IS_Events.txt. The original author is Wolfgang Weh.

[Setup]
OutputBaseFilename=TestEventsSetup
AppName=TestEvents
AppVerName=TestEvents V1.1
DefaultDirName={pf}\TestEvents
AppID=TestEvents
ShowTasksTreeLines=false
RestartIfNeededByRun=false
UserInfoPage=true
DefaultGroupName=TestEvents
InfoAfterFile=.\InfoAfterFile.txt
InfoBeforeFile=.\InfoBeforeFile.txt
LicenseFile=.\LicenseFile.txt
OutputDir=.\
WindowVisible=true
UninstallLogMode=append


[Files]
Source: Test Events.iss; DestDir: {app}
Source: LicenseFile.txt; DestDir: {app}
Source: InfoBeforeFile.txt; DestDir: {app}
Source: InfoAfterFile.txt; DestDir: {app}


[Icons]
Name: {group}\Uninstall Test Events; Filename: {uninstallexe}
Name: {group}\Test Events; Filename: {app}\Test Events.iss; WorkingDir: {app}; Comment: Test Events; Flags: createonlyiffileexists



[Messages]
BeveledLabel=Test Events Setup



[Code]
var
	mcLogFile: String;


//  Install Event Functions
//==========================


function InitializeSetup(): Boolean;
// function InitializeSetup(): Boolean;
// Called during Setup's initialization. Return False to abort Setup, True otherwise.
begin
	mcLogFile := 'C:\Log_IS_Events.txt';
	DeleteFile( mcLogFile);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, mcLogFile + ' - logfile of InnoSetups install and uninstall events.' + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'Start installation: ' + GetDateTimeString('dd/mm/yyyy hh:nn:ss', '.', ':') + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'In function InitializeSetup' + Chr(13), True);
	Result := True;
end;



procedure InitializeWizard();
// procedure InitializeWizard();
// Use this event function to make changes to the wizard or wizard pages at startup.
// You can't use the InitializeSetup event function for this since at the time
// it is triggered, the wizard form does not yet exist.
begin
	SaveStringToFile(mcLogFile, 'In procedure InitializeWizard' + Chr(13), True);
end;



procedure DeInitializeSetup();
// procedure DeinitializeSetup();
// Called just before Setup terminates. Note that this function is called even
// if the user exits Setup before anything is installed.
begin
	SaveStringToFile(mcLogFile, 'In procedure DeInitializeSetup' + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'End installation: ' + GetDateTimeString('dd/mm/yyyy hh:nn:ss', '.', ':') + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
end;



procedure CurStepChanged(CurStep: TSetupStep);
// procedure CurStepChanged(CurStep: TSetupStep);
// You can this event function to perform your own pre-install and post-install tasks.
// Called with CurStep=ssInstall just before the actual installation starts,
// with CurStep=ssPostInstall just after the actual installation finishes,
// and with CurStep=ssDone just before Setup terminates after a successful install.
begin
	SaveStringToFile(mcLogFile, 'In procedure CurStepChanged, CurStep=', True);
	case CurStep of
		ssInstall :
		  SaveStringToFile(mcLogFile, ' ssInstall' + Chr(13), True);
		ssPostInstall :
		  SaveStringToFile(mcLogFile, ' ssPostInstall' + Chr(13), True);
		ssDone :
		  SaveStringToFile(mcLogFile, ' ssDone' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' no idea' + Chr(13), True);
	end;
end;



function NextButtonClick(CurPageID: Integer): Boolean;
// function NextButtonClick(CurPageID: Integer): Boolean;
// Called when the user clicks the Next button.
// If you return True, the wizard will move to the next page;
// if you return False, it will remain on the current page (specified by CurPageID).
// Note that this function is called on silent installs as well,
// even though there is no Next button that the user can click.
// Setup instead simulates "clicks" on the Next button.
// On a silent install, if your NextButtonClick function returns False prior to installation starting,
// Setup will exit automatically.

// CurPageID values for predefined wizard pages
//----------------------------------------------
// wpWelcome, wpLicense, wpPassword, wpInfoBefore, wpUserInfo, wpSelectDir, wpSelectComponents,
// wpSelectProgramGroup, wpSelectTasks, wpReady, wpPreparing, wpInstalling, wpInfoAfter, wpFinished

begin
	SaveStringToFile(mcLogFile, 'In function NextButtonClick, CurPageID=' + IntToStr(CurPageID), True);
	case CurPageID of
		wpWelcome :
		  SaveStringToFile(mcLogFile, ' : wpWelcome' + Chr(13), True);
		wpLicense :
		  SaveStringToFile(mcLogFile, ' : wpLicense' + Chr(13), True);
		wpPassword :
		  SaveStringToFile(mcLogFile, ' : wpPassword' + Chr(13), True);
		wpInfoBefore :
		  SaveStringToFile(mcLogFile, ' : wpInfoBefore' + Chr(13), True);
		wpUserInfo :
		  SaveStringToFile(mcLogFile, ' : wpUserInfo' + Chr(13), True);
		wpSelectDir :
		  SaveStringToFile(mcLogFile, ' : wpSelectDir' + Chr(13), True);
		wpSelectComponents :
		  SaveStringToFile(mcLogFile, ' : wpSelectComponents' + Chr(13), True);
		wpSelectProgramGroup :
		  SaveStringToFile(mcLogFile, ' : wpSelectProgramGroup' + Chr(13), True);
		wpSelectTasks :
		  SaveStringToFile(mcLogFile, ' : wpSelectTasks' + Chr(13), True);
		wpReady :
		  SaveStringToFile(mcLogFile, ' : wpReady' + Chr(13), True);
		wpPreparing :
		  SaveStringToFile(mcLogFile, ' : wpPreparing' + Chr(13), True);
		wpInstalling :
		  SaveStringToFile(mcLogFile, ' : wpInstalling' + Chr(13), True);
		wpInfoAfter :
		  SaveStringToFile(mcLogFile, ' : wpInfoAfter' + Chr(13), True);
		wpFinished :
		  SaveStringToFile(mcLogFile, ' : wpFinished' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' : no idea' + Chr(13), True);
	end;
	Result := True;
end;




function BackButtonClick(CurPageID: Integer): Boolean;
// function BackButtonClick(CurPageID: Integer): Boolean;
// Called when the user clicks the Back button.
// If you return True, the wizard will move to the previous page;
// if you return False, it will remain on the current page (specified by CurPageID).

// CurPageID values for predefined wizard pages
//----------------------------------------------
// wpWelcome, wpLicense, wpPassword, wpInfoBefore, wpUserInfo, wpSelectDir, wpSelectComponents,
// wpSelectProgramGroup, wpSelectTasks, wpReady, wpPreparing, wpInstalling, wpInfoAfter, wpFinished

begin
	SaveStringToFile(mcLogFile, 'In function BackButtonClick, CurPageID=' + IntToStr(CurPageID), True);
	case CurPageID of
		wpWelcome :
		  SaveStringToFile(mcLogFile, ' : wpWelcome' + Chr(13), True);
		wpLicense :
		  SaveStringToFile(mcLogFile, ' : wpLicense' + Chr(13), True);
		wpPassword :
		  SaveStringToFile(mcLogFile, ' : wpPassword' + Chr(13), True);
		wpInfoBefore :
		  SaveStringToFile(mcLogFile, ' : wpInfoBefore' + Chr(13), True);
		wpUserInfo :
		  SaveStringToFile(mcLogFile, ' : wpUserInfo' + Chr(13), True);
		wpSelectDir :
		  SaveStringToFile(mcLogFile, ' : wpSelectDir' + Chr(13), True);
		wpSelectComponents :
		  SaveStringToFile(mcLogFile, ' : wpSelectComponents' + Chr(13), True);
		wpSelectProgramGroup :
		  SaveStringToFile(mcLogFile, ' : wpSelectProgramGroup' + Chr(13), True);
		wpSelectTasks :
		  SaveStringToFile(mcLogFile, ' : wpSelectTasks' + Chr(13), True);
		wpReady :
		  SaveStringToFile(mcLogFile, ' : wpReady' + Chr(13), True);
		wpPreparing :
		  SaveStringToFile(mcLogFile, ' : wpPreparing' + Chr(13), True);
		wpInstalling :
		  SaveStringToFile(mcLogFile, ' : wpInstalling' + Chr(13), True);
		wpInfoAfter :
		  SaveStringToFile(mcLogFile, ' : wpInfoAfter' + Chr(13), True);
		wpFinished :
		  SaveStringToFile(mcLogFile, ' : wpFinished' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' : no idea' + Chr(13), True);
	end;
	Result := True;
end;




procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean);
// procedure CancelButtonClick(CurPageID: Integer; var Cancel, Confirm: Boolean);
// Called when the user clicks the Cancel button or clicks the window's Close button.
// The Cancel parameter specifies whether normal cancel processing should occur; it defaults to True.
// The Confirm parameter specifies whether an "Exit Setup?" message box should be displayed; it usually defaults to True.
// If Cancel is set to False, then the value of Confirm is ignored.

// CurPageID values for predefined wizard pages
//----------------------------------------------
// wpWelcome, wpLicense, wpPassword, wpInfoBefore, wpUserInfo, wpSelectDir, wpSelectComponents,
// wpSelectProgramGroup, wpSelectTasks, wpReady, wpPreparing, wpInstalling, wpInfoAfter, wpFinished

var
  cTemp: String;
begin
	SaveStringToFile(mcLogFile, 'In function CancelButtonClick, CurPageID=' + IntToStr(CurPageID), True);
	if Cancel then cTemp:='True' else cTemp:='False';
	SaveStringToFile(mcLogFile, ', Cancel=' + cTemp, True);
	if Confirm then cTemp:='True' else cTemp:='False';
	SaveStringToFile(mcLogFile, ', Confirm=' + cTemp, True);
	case CurPageID of
		wpWelcome :
		  SaveStringToFile(mcLogFile, ' : wpWelcome' + Chr(13), True);
		wpLicense :
		  SaveStringToFile(mcLogFile, ' : wpLicense' + Chr(13), True);
		wpPassword :
		  SaveStringToFile(mcLogFile, ' : wpPassword' + Chr(13), True);
		wpInfoBefore :
		  SaveStringToFile(mcLogFile, ' : wpInfoBefore' + Chr(13), True);
		wpUserInfo :
		  SaveStringToFile(mcLogFile, ' : wpUserInfo' + Chr(13), True);
		wpSelectDir :
		  SaveStringToFile(mcLogFile, ' : wpSelectDir' + Chr(13), True);
		wpSelectComponents :
		  SaveStringToFile(mcLogFile, ' : wpSelectComponents' + Chr(13), True);
		wpSelectProgramGroup :
		  SaveStringToFile(mcLogFile, ' : wpSelectProgramGroup' + Chr(13), True);
		wpSelectTasks :
		  SaveStringToFile(mcLogFile, ' : wpSelectTasks' + Chr(13), True);
		wpReady :
		  SaveStringToFile(mcLogFile, ' : wpReady' + Chr(13), True);
		wpPreparing :
		  SaveStringToFile(mcLogFile, ' : wpPreparing' + Chr(13), True);
		wpInstalling :
		  SaveStringToFile(mcLogFile, ' : wpInstalling' + Chr(13), True);
		wpInfoAfter :
		  SaveStringToFile(mcLogFile, ' : wpInfoAfter' + Chr(13), True);
		wpFinished :
		  SaveStringToFile(mcLogFile, ' : wpFinished' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' : no idea' + Chr(13), True);
	end;
end;



function ShouldSkipPage(PageID: Integer): Boolean;
// function ShouldSkipPage(PageID: Integer): Boolean;
// The wizard calls this event function to determine whether or not a particular page
// (specified by PageID) should be shown at all.
// If you return True, the page will be skipped;
// if you return False, the page may be shown.
// Note: This event function isn't called for the wpWelcome, wpPreparing, and wpInstalling pages,
// not for pages that Setup has already determined should be skipped
// (for example, wpSelectComponents in an install containing no components).
begin
	SaveStringToFile(mcLogFile, 'In function ShouldSkipPage, PageID=' + IntToStr(PageID), True);
	case PageID of
		wpWelcome :
		  SaveStringToFile(mcLogFile, ' : wpWelcome' + Chr(13), True);
		wpLicense :
		  SaveStringToFile(mcLogFile, ' : wpLicense' + Chr(13), True);
		wpPassword :
		  SaveStringToFile(mcLogFile, ' : wpPassword' + Chr(13), True);
		wpInfoBefore :
		  SaveStringToFile(mcLogFile, ' : wpInfoBefore' + Chr(13), True);
		wpUserInfo :
		  SaveStringToFile(mcLogFile, ' : wpUserInfo' + Chr(13), True);
		wpSelectDir :
		  SaveStringToFile(mcLogFile, ' : wpSelectDir' + Chr(13), True);
		wpSelectComponents :
		  SaveStringToFile(mcLogFile, ' : wpSelectComponents' + Chr(13), True);
		wpSelectProgramGroup :
		  SaveStringToFile(mcLogFile, ' : wpSelectProgramGroup' + Chr(13), True);
		wpSelectTasks :
		  SaveStringToFile(mcLogFile, ' : wpSelectTasks' + Chr(13), True);
		wpReady :
		  SaveStringToFile(mcLogFile, ' : wpReady' + Chr(13), True);
		wpPreparing :
		  SaveStringToFile(mcLogFile, ' : wpPreparing' + Chr(13), True);
		wpInstalling :
		  SaveStringToFile(mcLogFile, ' : wpInstalling' + Chr(13), True);
		wpInfoAfter :
		  SaveStringToFile(mcLogFile, ' : wpInfoAfter' + Chr(13), True);
		wpFinished :
		  SaveStringToFile(mcLogFile, ' : wpFinished' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' : no idea' + Chr(13), True);
	end;
	Result := False;
end;



procedure CurPageChanged(CurPageID: Integer);
// procedure CurPageChanged(CurPageID: Integer);
// Called after a new wizard page (specified by CurPageID) is shown.

// CurPageID values for predefined wizard pages
//----------------------------------------------
// wpWelcome, wpLicense, wpPassword, wpInfoBefore, wpUserInfo, wpSelectDir, wpSelectComponents,
// wpSelectProgramGroup, wpSelectTasks, wpReady, wpPreparing, wpInstalling, wpInfoAfter, wpFinished

begin
	SaveStringToFile(mcLogFile, 'In procedure CurPageChanged, CurPageID=' + IntToStr(CurPageID), True);
	case CurPageID of
		wpWelcome :
		  SaveStringToFile(mcLogFile, ' : wpWelcome' + Chr(13), True);
		wpLicense :
		  SaveStringToFile(mcLogFile, ' : wpLicense' + Chr(13), True);
		wpPassword :
		  SaveStringToFile(mcLogFile, ' : wpPassword' + Chr(13), True);
		wpInfoBefore :
		  SaveStringToFile(mcLogFile, ' : wpInfoBefore' + Chr(13), True);
		wpUserInfo :
		  SaveStringToFile(mcLogFile, ' : wpUserInfo' + Chr(13), True);
		wpSelectDir :
		  SaveStringToFile(mcLogFile, ' : wpSelectDir' + Chr(13), True);
		wpSelectComponents :
		  SaveStringToFile(mcLogFile, ' : wpSelectComponents' + Chr(13), True);
		wpSelectProgramGroup :
		  SaveStringToFile(mcLogFile, ' : wpSelectProgramGroup' + Chr(13), True);
		wpSelectTasks :
		  SaveStringToFile(mcLogFile, ' : wpSelectTasks' + Chr(13), True);
		wpReady :
		  SaveStringToFile(mcLogFile, ' : wpReady' + Chr(13), True);
		wpPreparing :
		  SaveStringToFile(mcLogFile, ' : wpPreparing' + Chr(13), True);
		wpInstalling :
		  SaveStringToFile(mcLogFile, ' : wpInstalling' + Chr(13), True);
		wpInfoAfter :
		  SaveStringToFile(mcLogFile, ' : wpInfoAfter' + Chr(13), True);
		wpFinished :
		  SaveStringToFile(mcLogFile, ' : wpFinished' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' : no idea' + Chr(13), True);
	end;
end;



function CheckPassword(Password: String): Boolean;
// function CheckPassword(Password: String): Boolean;
// If Setup finds the CheckPassword event function in the Pascal script, it automatically
// displays the Password page and calls CheckPassword to check passwords.
// Return True to accept the password and False to reject it.
//	To avoid storing the actual password inside the compiled [Code] section which is stored inside Setup,
// you should use comparisons by hash only: calculate the MD5 hash of your password yourself and then
// compare that to GetMD5OfString(Password).
// This way the actual value of the password remains protected.
//
// Note: if you have a CheckPassword event function and your users run Setup with both
// the "/PASSWORD=" and "/SILENT" command line parameters set, your CheckPassword function
// will be called *before* any other event function is called, including InitializeSetup.
begin
	SaveStringToFile(mcLogFile, 'In function CheckPassword, Password=' + Password + Chr(13), True);
	Result := True;
end;



function NeedRestart(): Boolean;
// function NeedRestart(): Boolean;
// Return True to instruct Setup to prompt the user to restart the system at the
// end of a successful installation, False otherwise.
begin
	SaveStringToFile(mcLogFile, 'In function NeedRestart' + Chr(13), True);
	Result := False;
end;



function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
// function UpdateReadyMemo(Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo,
//                          MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String;
// If Setup finds the UpdateReadyMemo event function in the Pascal script, it is called automatically
// when the Ready to Install wizard page becomes the active page.
// It should return the text to be displayed in the settings memo on the Ready to Install wizard page
// as a single string with lines separated by the NewLine parameter.
// Parameter Space contains a string with spaces. Setup uses this string to indent settings.
// The other parameters contain the (possibly empty) strings that Setup would have used as the setting sections.
// The MemoDirInfo parameter for example contains the string for the Selected Directory section.
var
	cTemp: String;
begin
	SaveStringToFile(mcLogFile, 'In function UpdateReadyMemo' + Chr(13), True);
	cTemp := MemoUserInfoInfo + NewLine;
	cTemp := cTemp + MemoDirInfo + NewLine;
	cTemp := cTemp + MemoTypeInfo + NewLine;
	cTemp := cTemp + MemoComponentsInfo + NewLine;
	cTemp := cTemp + MemoGroupInfo + NewLine;
	cTemp := cTemp + MemoTasksInfo + NewLine;
	Result := cTemp;
end;



procedure RegisterPreviousData(PreviousDataKey: Integer);
// procedure RegisterPreviousData(PreviousDataKey: Integer);
// To store user settings entered on custom wizard pages, place a RegisterPreviousData event function
// in the Pascal script and call SetPreviousData(PreviousDataKey, ...) inside it, once per setting.
begin
	SaveStringToFile(mcLogFile, 'In procedure PreviousDataKey, PreviousDataKey=' + Chr(13), True);
end;



function CheckSerial(Serial: String): Boolean;
// function CheckSerial(Serial: String): Boolean;
// If Setup finds the CheckSerial event function in the Pascal script, a serial number field will
// automatically appear on the User Info wizard page (which must be enabled using UserInfoPage=yes in your [Setup] section!).
// Return True to accept the serial number and False to reject it.
// When using serial numbers, it's important to keep in mind that since no encryption is used and the source code to
// Inno Setup is freely available, it would not be too difficult for an experienced individual to remove the serial
// number protection from an installation. Use this only as a convienience to the end user and double check the entered
// serial number (stored in the {userinfoserial} constant) in your application.
begin
	SaveStringToFile(mcLogFile, 'In function CheckSerial, Serial=' + Serial + Chr(13), True);
	Result := True;
end;



function GetCustomSetupExitCode: Integer;
// function GetCustomSetupExitCode: Integer;
// Return a non zero number to instruct Setup to return a custom exit code.
// This function is only called if Setup was successfully run to completion and
// the exit code would have been 0. Also see Setup Exit Codes.
begin
	SaveStringToFile(mcLogFile, 'In function GetCustomSetupExitCode' + Chr(13), True);
	Result := 0;
end;




//  UNinstall Event Functions
//============================
function InitializeUninstall(): Boolean;
// function InitializeUninstall(): Boolean;
// Return False to abort Uninstall, True otherwise.
begin
	mcLogFile := 'C:\Log_IS_Events.txt';
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, mcLogFile + ' - logfile of InnoSetups install and uninstall events.' + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'Start Uninstallation: ' + GetDateTimeString('dd/mm/yyyy hh:nn:ss', '.', ':') + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'In function InitializeUninstall' + Chr(13), True);
	Result := True;
end;



procedure DeinitializeUninstall();
// procedure DeinitializeUninstall();
begin
	SaveStringToFile(mcLogFile, 'In function DeinitializeUninstall' + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
	SaveStringToFile(mcLogFile, 'End Uninstallation: ' + GetDateTimeString('dd/mm/yyyy hh:nn:ss', '.', ':') + Chr(13), True);
	SaveStringToFile(mcLogFile, Chr(13), True);
end;



procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
// procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
// You can this event function to perform your own pre-UNinstall and post-UNinstall tasks.
begin
	SaveStringToFile(mcLogFile, 'In procedure CurUninstallStepChanged, CurUninstallStep=', True);
	case CurUninstallStep of
		usAppMutexCheck :
		  SaveStringToFile(mcLogFile, ' usAppMutexCheck' + Chr(13), True);
		usUninstall :
		  SaveStringToFile(mcLogFile, ' usUninstall' + Chr(13), True);
		usPostUninstall :
		  SaveStringToFile(mcLogFile, ' usPostUninstall' + Chr(13), True);
		usDone :
		  SaveStringToFile(mcLogFile, ' usDone' + Chr(13), True);
	else
		SaveStringToFile(mcLogFile, ' no idea' + Chr(13), True);
	end;
end;



function UninstallNeedRestart(): Boolean;
// function UninstallNeedRestart(): Boolean;
// Return True to instruct Uninstall to prompt the user to restart the system at
// the end of a successful uninstallation, False otherwise.
begin
	SaveStringToFile(mcLogFile, 'In function UninstallNeedRestart' + Chr(13), True);
	Result := False;
end;




//  Here's the list of constants used by these functions
//=======================================================


// CurStep values
//----------------
// ssInstall, ssPostInstall, ssDone


// CurUninstallStep values
//-------------------------
// usAppMutexCheck, usUninstall, usPostUninstall, usDone


// CurPageID values for predefined wizard pages
//----------------------------------------------
// wpWelcome, wpLicense, wpPassword, wpInfoBefore, wpUserInfo, wpSelectDir, wpSelectComponents,
// wpSelectProgramGroup, wpSelectTasks, wpReady, wpPreparing, wpInstalling, wpInfoAfter, wpFinished


// None of these functions are required to be present in a Pascal script.

See Also

Test events example

Personal tools
Ads: